aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2015-07-08 07:35:06 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2015-07-08 07:35:06 -0300
commit055d72d76b44b0e627c8a17c48dbecd62e44197b (patch)
treee2c8d5475477c46115461fe9547c1ee797873635 /nikola/plugins/task
parent61f3aad02cd6492cb38e41b66f2ed8ec56e98981 (diff)
parentb0b24795b24ee6809397fbbadf42f31f310a219f (diff)
Merge tag 'upstream/7.6.0'
Upstream version 7.6.0
Diffstat (limited to 'nikola/plugins/task')
-rw-r--r--nikola/plugins/task/__init__.py2
-rw-r--r--nikola/plugins/task/archive.plugin2
-rw-r--r--nikola/plugins/task/archive.py248
-rw-r--r--nikola/plugins/task/bundles.plugin2
-rw-r--r--nikola/plugins/task/bundles.py23
-rw-r--r--nikola/plugins/task/copy_assets.plugin2
-rw-r--r--nikola/plugins/task/copy_assets.py6
-rw-r--r--nikola/plugins/task/copy_files.plugin2
-rw-r--r--nikola/plugins/task/copy_files.py4
-rw-r--r--nikola/plugins/task/galleries.plugin2
-rw-r--r--nikola/plugins/task/galleries.py312
-rw-r--r--nikola/plugins/task/gzip.plugin2
-rw-r--r--nikola/plugins/task/gzip.py2
-rw-r--r--nikola/plugins/task/indexes.plugin2
-rw-r--r--nikola/plugins/task/indexes.py123
-rw-r--r--nikola/plugins/task/listings.plugin2
-rw-r--r--nikola/plugins/task/listings.py266
-rw-r--r--nikola/plugins/task/pages.plugin2
-rw-r--r--nikola/plugins/task/pages.py6
-rw-r--r--nikola/plugins/task/posts.plugin2
-rw-r--r--nikola/plugins/task/posts.py63
-rw-r--r--nikola/plugins/task/redirect.plugin2
-rw-r--r--nikola/plugins/task/redirect.py23
-rw-r--r--nikola/plugins/task/robots.plugin2
-rw-r--r--nikola/plugins/task/robots.py11
-rw-r--r--nikola/plugins/task/rss.plugin2
-rw-r--r--nikola/plugins/task/rss.py18
-rw-r--r--nikola/plugins/task/scale_images.plugin9
-rw-r--r--nikola/plugins/task/scale_images.py96
-rw-r--r--nikola/plugins/task/sitemap.plugin2
-rw-r--r--nikola/plugins/task/sitemap/__init__.py106
-rw-r--r--nikola/plugins/task/sources.plugin2
-rw-r--r--nikola/plugins/task/sources.py7
-rw-r--r--nikola/plugins/task/tags.plugin2
-rw-r--r--nikola/plugins/task/tags.py301
35 files changed, 1026 insertions, 632 deletions
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/task/__init__.py
+++ b/nikola/plugins/task/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin
index 448b115..6687209 100644
--- a/nikola/plugins/task/archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -4,7 +4,7 @@ Module = archive
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generates the blog's archive pages.
diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py
index 4f1ab19..533be69 100644
--- a/nikola/plugins/task/archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,12 +24,14 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import copy
import os
# for tearDown with _reload we cannot use 'import from' to access LocaleBorg
import nikola.utils
+import datetime
from nikola.plugin_categories import Task
-from nikola.utils import config_changed
+from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name_for_index_link
class Archive(Task):
@@ -39,133 +41,191 @@ class Archive(Task):
def set_site(self, site):
site.register_path_handler('archive', self.archive_path)
+ site.register_path_handler('archive_atom', self.archive_atom_path)
return super(Archive, self).set_site(site)
+ def _prepare_task(self, kw, name, lang, posts, items, template_name,
+ title, deps_translatable=None):
+ # name: used to build permalink and destination
+ # posts, items: posts or items; only one of them should be used,
+ # the other be None
+ # template_name: name of the template to use
+ # title: the (translated) title for the generated page
+ # deps_translatable: dependencies (None if not added)
+ assert posts is not None or items is not None
+
+ context = {}
+ context["lang"] = lang
+ context["title"] = title
+ context["permalink"] = self.site.link("archive", name, lang)
+ if posts is not None:
+ context["posts"] = posts
+ n = len(posts)
+ else:
+ context["items"] = items
+ n = len(items)
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ os.path.join(kw['output_folder'], self.site.path("archive", name, lang)),
+ template_name,
+ kw['filters'],
+ context,
+ )
+
+ task_cfg = {1: copy.copy(kw), 2: n}
+ if deps_translatable is not None:
+ task_cfg[3] = deps_translatable
+ task['uptodate'] = task['uptodate'] + [config_changed(task_cfg, 'nikola.plugins.task.archive')]
+ task['basename'] = self.name
+ return task
+
+ def _generate_posts_task(self, kw, name, lang, posts, title, deps_translatable=None):
+ posts = sorted(posts, key=lambda a: a.date)
+ posts.reverse()
+ if kw['archives_are_indexes']:
+ def page_link(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return adjust_name_for_index_link(self.site.link("archive" + feed, name, lang), i, displayed_i,
+ lang, self.site, force_addition, extension)
+
+ def page_path(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return adjust_name_for_index_path(self.site.path("archive" + feed, name, lang), i, displayed_i,
+ lang, self.site, force_addition, extension)
+
+ uptodate = []
+ if deps_translatable is not None:
+ uptodate += [config_changed(deps_translatable, 'nikola.plugins.task.archive')]
+ yield self.site.generic_index_renderer(
+ lang,
+ posts,
+ title,
+ "archiveindex.tmpl",
+ {"archive_name": name,
+ "is_feed_stale": kw["is_feed_stale"]},
+ kw,
+ str(self.name),
+ page_link,
+ page_path,
+ uptodate)
+ else:
+ yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable)
+
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'],
+ "archives_are_indexes": self.site.config['ARCHIVES_ARE_INDEXES'],
"create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
"create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'],
+ "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
+ "create_full_archives": self.site.config['CREATE_FULL_ARCHIVES'],
+ "create_daily_archive": self.site.config['CREATE_DAILY_ARCHIVE'],
+ "pretty_urls": self.site.config['PRETTY_URLS'],
+ "strip_indexes": self.site.config['STRIP_INDEXES'],
+ "index_file": self.site.config['INDEX_FILE'],
+ "generate_atom": self.site.config["GENERATE_ATOM"],
}
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']:
+ if (kw['create_monthly_archive'] and kw['create_single_archive']) and not kw['create_full_archives']:
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}
+ if kw['create_single_archive'] and not kw['create_full_archives']:
+ # if we are creating one single archive
+ archdata = {}
+ else:
+ # if we are not creating one single archive, start with all years
+ archdata = self.site.posts_per_year.copy()
+ if kw['create_single_archive'] or kw['create_full_archives']:
+ # if we are creating one single archive, or full archives
+ archdata[None] = self.site.posts # for create_single_archive
for year, posts in archdata.items():
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", year, lang))
- context = {}
- context["lang"] = lang
+ # Filter untranslated posts (Issue #1360)
+ if not kw["show_untranslated_posts"]:
+ posts = [p for p in posts if lang in p.translated_to]
+
+ # Add archive per year or total archive
if year:
- context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ title = kw["messages"][lang]["Posts for year %s"] % year
+ kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != 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 = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- context["posts"] = post_list
- else: # Monthly archives, just list the months
- months = set([(m.split('/')[1], self.site.link("archive", m, lang)) 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), link] for month, link in months]
- post_list = []
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- n = len(post_list) if 'posts' in context else len(months)
-
+ title = kw["messages"][lang]["Archive"]
+ kw["is_feed_stale"] = False
deps_translatable = {}
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
deps_translatable[k] = self.site.GLOBAL_CONTEXT[k](lang)
+ if not kw["create_monthly_archive"] or kw["create_full_archives"]:
+ yield self._generate_posts_task(kw, year, lang, posts, title, deps_translatable)
+ else:
+ months = set([(m.split('/')[1], self.site.link("archive", m, lang)) for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
+ months = sorted(list(months))
+ months.reverse()
+ items = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link] for month, link in months]
+ yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable)
- task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: n, 4: deps_translatable}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- if not kw["create_monthly_archive"]:
+ if not kw["create_monthly_archive"] and not kw["create_full_archives"] and not kw["create_daily_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))
+ # Add archive per month
year, month = yearmonth.split('/')
- post_list = sorted(posts, 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, 3: len(post_list)}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- if not kw['create_single_archive']:
+
+ kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m") != yearmonth)
+
+ # Filter untranslated posts (via Issue #1360)
+ if not kw["show_untranslated_posts"]:
+ posts = [p for p in posts if lang in p.translated_to]
+
+ if kw["create_monthly_archive"] or kw["create_full_archives"]:
+ title = kw["messages"][lang]["Posts for {month} {year}"].format(
+ year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
+ yield self._generate_posts_task(kw, yearmonth, lang, posts, title)
+
+ if not kw["create_full_archives"] and not kw["create_daily_archive"]:
+ continue # Just to avoid nesting the other loop in this if
+ # Add archive per day
+ days = dict()
+ for p in posts:
+ if p.date.day not in days:
+ days[p.date.day] = list()
+ days[p.date.day].append(p)
+ for day, posts in days.items():
+ title = kw["messages"][lang]["Posts for {month} {day}, {year}"].format(
+ year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang), day=day)
+ yield self._generate_posts_task(kw, yearmonth + '/{0:02d}'.format(day), lang, posts, title)
+
+ if not kw['create_single_archive'] and not kw['create_full_archives']:
# And an "all your years" page for yearly and monthly archives
+ if "is_feed_stale" in kw:
+ del kw["is_feed_stale"]
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"] = [(y, self.site.link("archive", y, lang))
- for y 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, 3: len(years)}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- def archive_path(self, name, lang):
+ items = [(y, self.site.link("archive", y, lang)) for y in years]
+ yield self._prepare_task(kw, None, lang, None, items, "list.tmpl", kw["messages"][lang]["Archive"])
+
+ def archive_path(self, name, lang, is_feed=False):
+ if is_feed:
+ extension = ".atom"
+ archive_file = os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0] + extension
+ index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
+ else:
+ archive_file = self.site.config['ARCHIVE_FILENAME']
+ index_file = self.site.config['INDEX_FILE']
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]
+ 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]
+ archive_file] if _f]
+
+ def archive_atom_path(self, name, lang):
+ return self.archive_path(name, lang, is_feed=True)
diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin
index e0b0a4d..3fe049b 100644
--- a/nikola/plugins/task/bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -4,7 +4,7 @@ Module = bundles
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Theme bundles using WebAssets
diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py
index fca6924..6f88d0c 100644
--- a/nikola/plugins/task/bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -43,11 +43,12 @@ class BuildBundles(LateTask):
name = "create_bundles"
def set_site(self, site):
- super(BuildBundles, self).set_site(site)
- if webassets is None and self.site.config['USE_BUNDLES']:
+ self.logger = utils.get_logger('bundles', site.loghandlers)
+ if webassets is None and site.config['USE_BUNDLES']:
utils.req_missing(['webassets'], 'USE_BUNDLES', optional=True)
- utils.LOGGER.warn('Setting USE_BUNDLES to False.')
- self.site.config['USE_BUNDLES'] = False
+ self.logger.warn('Setting USE_BUNDLES to False.')
+ site.config['USE_BUNDLES'] = False
+ super(BuildBundles, self).set_site(site)
def gen_tasks(self):
"""Bundle assets using WebAssets."""
@@ -74,7 +75,12 @@ class BuildBundles(LateTask):
bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
env.register(output, bundle)
# This generates the file
- env[output].urls()
+ try:
+ env[output].urls()
+ except Exception as e:
+ self.logger.error("Failed to build bundles.")
+ self.logger.exception(e)
+ self.logger.notice("Try running ``nikola clean`` and building again.")
else:
with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'):
pass # Create empty file
@@ -91,8 +97,7 @@ class BuildBundles(LateTask):
files.append(os.path.join(dname, fname))
file_dep = [os.path.join(kw['output_folder'], fname)
for fname in files if
- utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS'])
- or fname == 'assets/css/code.css']
+ utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS']) or fname == os.path.join('assets', 'css', 'code.css')]
# code.css will be generated by us if it does not exist in
# FILES_FOLDERS or theme assets. It is guaranteed that the
# generation will happen before this task.
@@ -107,7 +112,7 @@ class BuildBundles(LateTask):
utils.config_changed({
1: kw,
2: file_dep
- })],
+ }, 'nikola.plugins.task.bundles')],
'clean': True,
}
yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index 28b9e32..0530ebf 100644
--- a/nikola/plugins/task/copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -4,7 +4,7 @@ Module = copy_assets
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Copy theme assets into output.
diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py
index 29aa083..a72bfdf 100644
--- a/nikola/plugins/task/copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -72,7 +72,7 @@ class CopyAssets(Task):
if task['name'] in tasks:
continue
tasks[task['name']] = task
- task['uptodate'] = [utils.config_changed(kw)]
+ task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')]
task['basename'] = self.name
if code_css_input:
task['file_dep'] = [code_css_input]
@@ -99,7 +99,7 @@ class CopyAssets(Task):
'basename': self.name,
'name': code_css_path,
'targets': [code_css_path],
- 'uptodate': [utils.config_changed(kw), testcontents],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents],
'actions': [(create_code_css, [])],
'clean': True,
}
diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index 45c9e0d..073676b 100644
--- a/nikola/plugins/task/copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -4,7 +4,7 @@ Module = copy_files
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Copy static files into the output.
diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py
index 1d31756..9a039f1 100644
--- a/nikola/plugins/task/copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,5 +51,5 @@ class CopyFiles(Task):
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)]
+ task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_files')]
yield utils.apply_filters(task, filters, skip_ext=['.html'])
diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin
index 8352151..73085cd 100644
--- a/nikola/plugins/task/galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -4,7 +4,7 @@ Module = galleries
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create image galleries automatically.
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
index f835444..e887f18 100644
--- a/nikola/plugins/task/galleries.py
+++ b/nikola/plugins/task/galleries.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,34 +31,30 @@ import glob
import json
import mimetypes
import os
+import sys
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoin # NOQA
import natsort
-Image = None
try:
- from PIL import Image, ExifTags # NOQA
+ from PIL import Image # NOQA
except ImportError:
- try:
- import Image as _Image
- import ExifTags
- Image = _Image
- except ImportError:
- pass
+ import Image as _Image
+ Image = _Image
import PyRSS2Gen as rss
from nikola.plugin_categories import Task
from nikola import utils
+from nikola.image_processing import ImageProcessor
from nikola.post import Post
-from nikola.utils import req_missing
_image_size_cache = {}
-class Galleries(Task):
+class Galleries(Task, ImageProcessor):
"""Render image galleries."""
name = 'render_galleries'
@@ -66,47 +62,84 @@ class Galleries(Task):
def set_site(self, site):
site.register_path_handler('gallery', self.gallery_path)
+ site.register_path_handler('gallery_global', self.gallery_global_path)
site.register_path_handler('gallery_rss', self.gallery_rss_path)
+
+ self.logger = utils.get_logger('render_galleries', site.loghandlers)
+
+ self.kw = {
+ 'thumbnail_size': site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': site.config['OUTPUT_FOLDER'],
+ 'cache_folder': site.config['CACHE_FOLDER'],
+ 'default_lang': site.config['DEFAULT_LANG'],
+ 'use_filename_as_title': site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_folders': site.config['GALLERY_FOLDERS'],
+ 'sort_by_date': site.config['GALLERY_SORT_BY_DATE'],
+ 'filters': site.config['FILTERS'],
+ 'translations': site.config['TRANSLATIONS'],
+ 'global_context': site.GLOBAL_CONTEXT,
+ 'feed_length': site.config['FEED_LENGTH'],
+ 'tzinfo': site.tzinfo,
+ 'comments_in_galleries': site.config['COMMENTS_IN_GALLERIES'],
+ 'generate_rss': site.config['GENERATE_RSS'],
+ }
+
+ # Verify that no folder in GALLERY_FOLDERS appears twice
+ appearing_paths = set()
+ for source, dest in self.kw['gallery_folders'].items():
+ if source in appearing_paths or dest in appearing_paths:
+ problem = source if source in appearing_paths else dest
+ utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, exiting.".format(problem))
+ sys.exit(1)
+ appearing_paths.add(source)
+ appearing_paths.add(dest)
+
+ # Find all galleries we need to process
+ self.find_galleries()
+ # Create self.gallery_links
+ self.create_galleries_paths()
+
return super(Galleries, self).set_site(site)
+ def _find_gallery_path(self, name):
+ # The system using self.proper_gallery_links and self.improper_gallery_links
+ # is similar as in listings.py.
+ if name in self.proper_gallery_links:
+ return self.proper_gallery_links[name]
+ elif name in self.improper_gallery_links:
+ candidates = self.improper_gallery_links[name]
+ if len(candidates) == 1:
+ return candidates[0]
+ self.logger.error("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates))
+ else:
+ self.logger.error("Unknown gallery '{0}'!".format(name))
+ self.logger.info("Known galleries: " + str(list(self.proper_gallery_links.keys())))
+ sys.exit(1)
+
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]
+ gallery_path = self._find_gallery_path(name)
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] +
+ gallery_path.split(os.sep) +
+ [self.site.config['INDEX_FILE']] if _f]
+
+ def gallery_global_path(self, name, lang):
+ gallery_path = self._find_gallery_path(name)
+ return [_f for _f in gallery_path.split(os.sep) +
+ [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]
+ gallery_path = self._find_gallery_path(name)
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] +
+ gallery_path.split(os.sep) +
+ ['rss.xml'] if _f]
def gen_tasks(self):
"""Render image galleries."""
- if Image is None:
- req_missing(['pillow'], 'render 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 = self.image_ext_list_builtin
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'],
- 'tzinfo': self.site.tzinfo,
- 'comments_in_galleries': self.site.config['COMMENTS_IN_GALLERIES'],
- 'generate_rss': self.site.config['GENERATE_RSS'],
- }
-
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
self.kw['||template_hooks|{0}||'.format(k)] = v._items
@@ -114,22 +147,19 @@ class Galleries(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:
+ for gallery, input_folder, output_folder in self.gallery_list:
# Create subfolder list
folder_list = [(x, 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)
+ post = self.parse_index(gallery, input_folder, output_folder)
# Create image list, filter exclusions
image_list = self.get_image_list(gallery)
@@ -143,12 +173,12 @@ class Galleries(Task):
# Create thumbnails and large images in destination
for image in image_list:
- for task in self.create_target_images(image):
+ for task in self.create_target_images(image, input_folder):
yield task
# Remove excluded images
for image in self.get_excluded_images(gallery):
- for task in self.remove_excluded_image(image):
+ for task in self.remove_excluded_image(image, input_folder):
yield task
crumbs = utils.get_crumbs(gallery, index_folder=self)
@@ -160,9 +190,7 @@ class Galleries(Task):
dst = os.path.join(
self.kw['output_folder'],
- self.site.path(
- "gallery",
- os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ self.site.path("gallery", gallery, lang))
dst = os.path.normpath(dst)
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
@@ -187,25 +215,27 @@ class Galleries(Task):
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]
- dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in image_list]
+ thumbs = [os.path.join(self.kw['output_folder'], output_folder, os.path.relpath(t, input_folder)) for t in thumbs]
+ dst_img_list = [os.path.join(output_folder, os.path.relpath(t, input_folder)) for t in image_list]
+ dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in dst_img_list]
folders = []
# Generate friendly gallery names
for path, folder in folder_list:
- fpost = self.parse_index(path)
+ fpost = self.parse_index(path, input_folder, output_folder)
if fpost:
ft = fpost.title(lang) or folder
else:
ft = folder
+ if not folder.endswith('/'):
+ folder += '/'
folders.append((folder, ft))
- context["folders"] = natsort.natsorted(folders)
+ context["folders"] = natsort.natsorted(
+ folders, alg=natsort.ns.F | natsort.ns.IC)
context["crumbs"] = crumbs
- context["permalink"] = self.site.link(
- "gallery", os.path.basename(
- os.path.relpath(gallery, self.kw['gallery_path'])), lang)
+ context["permalink"] = self.site.link("gallery", gallery, lang)
context["enable_comments"] = self.kw['comments_in_galleries']
context["thumbnail_size"] = self.kw["thumbnail_size"]
@@ -216,15 +246,18 @@ class Galleries(Task):
'targets': [post.translated_base_path(lang)],
'file_dep': post.fragment_deps(lang),
'actions': [(post.compile, [lang])],
- 'uptodate': [utils.config_changed(self.kw)]
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:post')] + post.fragment_deps_uptodate(lang)
}
context['post'] = post
else:
context['post'] = None
file_dep = self.site.template_system.template_deps(
template_name) + image_list + thumbs
+ file_dep_dest = self.site.template_system.template_deps(
+ template_name) + dest_img_list + thumbs
if post:
file_dep += [post.translated_base_path(l) for l in self.kw['translations']]
+ file_dep_dest += [post.translated_base_path(l) for l in self.kw['translations']]
yield utils.apply_filters({
'basename': self.name,
@@ -244,58 +277,87 @@ class Galleries(Task):
'uptodate': [utils.config_changed({
1: self.kw,
2: self.site.config["COMMENTS_IN_GALLERIES"],
- 3: context,
- })],
+ 3: context.copy(),
+ }, 'nikola.plugins.task.galleries:gallery')],
}, self.kw['filters'])
# RSS for the gallery
if self.kw["generate_rss"]:
rss_dst = os.path.join(
self.kw['output_folder'],
- self.site.path(
- "gallery_rss",
- os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ self.site.path("gallery_rss", gallery, lang))
rss_dst = os.path.normpath(rss_dst)
yield utils.apply_filters({
'basename': self.name,
'name': rss_dst,
- 'file_dep': file_dep,
+ 'file_dep': file_dep_dest,
'targets': [rss_dst],
'actions': [
(self.gallery_rss, (
image_list,
+ dst_img_list,
img_titles,
lang,
- self.site.link(
- "gallery_rss", os.path.basename(gallery), lang),
+ self.site.link("gallery_rss", gallery, lang),
rss_dst,
context['title']
))],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw,
- })],
+ }, 'nikola.plugins.task.galleries:rss')],
}, 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'], followlinks=True):
- self.gallery_list.append(root)
+ for input_folder, output_folder in self.kw['gallery_folders'].items():
+ for root, dirs, files in os.walk(input_folder, followlinks=True):
+ self.gallery_list.append((root, input_folder, output_folder))
+
+ def create_galleries_paths(self):
+ """Given a list of galleries, puts their paths into self.gallery_links."""
+
+ # gallery_path is "gallery/foo/name"
+ self.proper_gallery_links = dict()
+ self.improper_gallery_links = dict()
+ for gallery_path, input_folder, output_folder in self.gallery_list:
+ if gallery_path == input_folder:
+ gallery_name = ''
+ # special case, because relpath will return '.' in this case
+ else:
+ gallery_name = os.path.relpath(gallery_path, input_folder)
+
+ output_path = os.path.join(output_folder, gallery_name)
+ self.proper_gallery_links[gallery_path] = output_path
+ self.proper_gallery_links[output_path] = output_path
+
+ # If the input and output names differ, the gallery is accessible
+ # only by `input` and `output/`.
+ output_path_noslash = output_path[:-1]
+ if output_path_noslash not in self.proper_gallery_links:
+ self.proper_gallery_links[output_path_noslash] = output_path
+
+ gallery_path_slash = gallery_path + '/'
+ if gallery_path_slash not in self.proper_gallery_links:
+ self.proper_gallery_links[gallery_path_slash] = output_path
+
+ if gallery_name not in self.improper_gallery_links:
+ self.improper_gallery_links[gallery_name] = list()
+ self.improper_gallery_links[gallery_name].append(output_path)
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'])
+ for gallery_path, input_folder, _ in self.gallery_list:
# 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)))
+ self.site.path("gallery", gallery_path)))
output_gallery = os.path.normpath(output_gallery)
# Task to create gallery in output/
yield {
@@ -304,16 +366,16 @@ class Galleries(Task):
'actions': [(utils.makedirs, (output_gallery,))],
'targets': [output_gallery],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:mkdir')],
}
- def parse_index(self, gallery):
+ def parse_index(self, gallery, input_folder, output_folder):
"""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)
+ self.kw["output_folder"], output_folder,
+ os.path.relpath(gallery, input_folder))
if os.path.isfile(index_path):
post = Post(
index_path,
@@ -361,12 +423,12 @@ class Galleries(Task):
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'])
+ def create_target_images(self, img, input_path):
+ gallery_name = os.path.dirname(img)
output_gallery = os.path.dirname(
os.path.join(
self.kw["output_folder"],
- self.site.path("gallery", gallery_name)))
+ self.site.path("gallery_global", gallery_name)))
# Do thumbnails and copy originals
# img is "galleries/name/image_name.jpg"
# img_name is "image_name.jpg"
@@ -392,7 +454,7 @@ class Galleries(Task):
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['thumbnail_size']
- })],
+ }, 'nikola.plugins.task.galleries:resize_thumb')],
}, self.kw['filters'])
yield utils.apply_filters({
@@ -407,19 +469,19 @@ class Galleries(Task):
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['max_image_size']
- })],
+ }, 'nikola.plugins.task.galleries:resize_max')],
}, self.kw['filters'])
- def remove_excluded_image(self, img):
+ def remove_excluded_image(self, img, input_folder):
# Remove excluded images
- # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path
+ # img is something like input_folder/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))))
+ self.site.path("gallery_global", os.path.dirname(img))))
+ img = os.path.relpath(img, input_folder)
img_path = os.path.join(output_folder, os.path.basename(img))
fname, ext = os.path.splitext(img_path)
thumb_path = fname + '.thumbnail' + ext
@@ -431,7 +493,7 @@ class Galleries(Task):
(utils.remove_file, (thumb_path,))
],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_thumb')],
}, self.kw['filters'])
yield utils.apply_filters({
@@ -441,7 +503,7 @@ class Galleries(Task):
(utils.remove_file, (img_path,))
],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_file')],
}, self.kw['filters'])
def render_gallery_index(
@@ -484,7 +546,7 @@ class Galleries(Task):
context['photo_array_json'] = json.dumps(photo_array)
self.site.render_template(template_name, output_name, context)
- def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title):
+ def gallery_rss(self, img_list, dest_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
@@ -492,10 +554,10 @@ class Galleries(Task):
"""
def make_url(url):
- return urljoin(self.site.config['BASE_URL'], url)
+ return urljoin(self.site.config['BASE_URL'], url.lstrip('/'))
items = []
- for img, title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]:
+ for img, srcimg, title in list(zip(dest_img_list, img_list, img_titles))[:self.kw["feed_length"]]:
img_size = os.stat(
os.path.join(
self.site.config['OUTPUT_FOLDER'], img)).st_size
@@ -503,7 +565,7 @@ class Galleries(Task):
'title': title,
'link': make_url(img),
'guid': rss.Guid(img, False),
- 'pubDate': self.image_date(img),
+ 'pubDate': self.image_date(srcimg),
'enclosure': rss.Enclosure(
make_url(img),
img_size,
@@ -515,12 +577,15 @@ class Galleries(Task):
title=title,
link=make_url(permalink),
description='',
- lastBuildDate=datetime.datetime.now(),
+ lastBuildDate=datetime.datetime.utcnow(),
items=items,
generator='http://getnikola.com/',
language=lang
)
+
rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
+ 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 io.open(output_path, "w+", encoding="utf-8") as rss_file:
@@ -528,66 +593,3 @@ class Galleries(Task):
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 as e:
- self.logger.warn("Can't thumbnail {0}, using original "
- "image as thumbnail ({1})".format(src, e))
- 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 in ('DateTimeOriginal', 'DateTimeDigitized'):
- 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
index b68ea6f..4867fd6 100644
--- a/nikola/plugins/task/gzip.plugin
+++ b/nikola/plugins/task/gzip.plugin
@@ -4,7 +4,7 @@ Module = gzip
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create gzipped copies of files
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
index bcc9637..5799839 100644
--- a/nikola/plugins/task/gzip.py
+++ b/nikola/plugins/task/gzip.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/task/indexes.plugin b/nikola/plugins/task/indexes.plugin
index a18942c..5d2bf5a 100644
--- a/nikola/plugins/task/indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -4,7 +4,7 @@ Module = indexes
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generates the blog's index pages.
diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py
index 0a2cd02..03d36b1 100644
--- a/nikola/plugins/task/indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,7 +29,7 @@ from collections import defaultdict
import os
from nikola.plugin_categories import Task
-from nikola.utils import config_changed
+from nikola import utils
class Indexes(Task):
@@ -39,6 +39,7 @@ class Indexes(Task):
def set_site(self, site):
site.register_path_handler('index', self.index_path)
+ site.register_path_handler('index_atom', self.index_atom_path)
return super(Indexes, self).set_site(site)
def gen_tasks(self):
@@ -47,85 +48,39 @@ class Indexes(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'],
"show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
+ "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'],
"indexes_title": self.site.config['INDEXES_TITLE'],
- "indexes_pages": self.site.config['INDEXES_PAGES'],
- "indexes_pages_main": self.site.config['INDEXES_PAGES_MAIN'],
"blog_title": self.site.config["BLOG_TITLE"],
- "rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"],
+ "generate_atom": self.site.config["GENERATE_ATOM"],
}
template_name = "index.tmpl"
posts = self.site.posts
+ self.number_of_pages = dict()
for lang in kw["translations"]:
- # Split in smaller lists
- lists = []
+ def page_link(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return utils.adjust_name_for_index_link(self.site.link("index" + feed, None, lang), i, displayed_i,
+ lang, self.site, force_addition, extension)
+
+ def page_path(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return utils.adjust_name_for_index_path(self.site.path("index" + feed, None, lang), i, displayed_i,
+ lang, self.site, force_addition, extension)
+
if kw["show_untranslated_posts"]:
filtered_posts = posts
else:
filtered_posts = [x for x in posts if x.is_translation_available(lang)]
- 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'](lang)
- if kw["indexes_pages_main"]:
- ipages_i = i + 1
- ipages_msg = "page %d"
- else:
- ipages_i = i
- ipages_msg = "old posts, page %d"
- if kw["indexes_pages"]:
- indexes_pages = kw["indexes_pages"] % ipages_i
- else:
- indexes_pages = " (" + \
- kw["messages"][lang][ipages_msg] % ipages_i + ")"
- if i > 0 or kw["indexes_pages_main"]:
- 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
+
+ indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang)
+ self.number_of_pages[lang] = (len(filtered_posts) + kw['index_display_post_count'] - 1) // kw['index_display_post_count']
+
+ yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, {}, kw, 'render_indexes', page_link, page_path)
if not self.site.config["STORY_INDEX"]:
return
@@ -135,6 +90,7 @@ class Indexes(Task):
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"index_file": self.site.config['INDEX_FILE'],
+ "strip_indexes": self.site.config['STRIP_INDEXES'],
}
template_name = "list.tmpl"
for lang in kw["translations"]:
@@ -151,6 +107,12 @@ class Indexes(Task):
should_render = True
output_name = os.path.join(kw['output_folder'], dirname, kw['index_file'])
short_destination = os.path.join(dirname, kw['index_file'])
+ link = short_destination.replace('\\', '/')
+ index_len = len(kw['index_file'])
+ if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
+ link = link[:-index_len]
+ context["permalink"] = link
+
for post in post_list:
# If there is an index.html pending to be created from
# a story, do not generate the STORY_INDEX
@@ -166,18 +128,25 @@ class Indexes(Task):
template_name,
kw['filters'],
context)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
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]
+ def index_path(self, name, lang, is_feed=False):
+ extension = None
+ if is_feed:
+ extension = ".atom"
+ index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['INDEX_PATH'],
- self.site.config['INDEX_FILE']]
- if _f]
+ index_file = self.site.config['INDEX_FILE']
+ return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['INDEX_PATH'],
+ index_file] if _f],
+ name,
+ utils.get_displayed_page_number(name, self.number_of_pages[lang], self.site),
+ lang,
+ self.site,
+ extension=extension)
+
+ def index_atom_path(self, name, lang):
+ return self.index_path(name, lang, is_feed=True)
diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin
index c93184d..a5ba77a 100644
--- a/nikola/plugins/task/listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -4,7 +4,7 @@ Module = listings
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Render code listings into output
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
index 79f6763..b913330 100644
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,74 +26,115 @@
from __future__ import unicode_literals, print_function
+import sys
import os
from pygments import highlight
from pygments.lexers import get_lexer_for_filename, TextLexer
-from pygments.formatters import HtmlFormatter
import natsort
-import re
from nikola.plugin_categories import Task
from nikola import utils
-# FIXME: (almost) duplicated with mdx_nikola.py
-CODERE = re.compile('<div class="code"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL)
-
-
class Listings(Task):
"""Render pretty listings."""
name = "render_listings"
+ def register_output_name(self, input_folder, rel_name, rel_output_name):
+ """Register proper and improper file mappings."""
+ if rel_name not in self.improper_input_file_mapping:
+ self.improper_input_file_mapping[rel_name] = []
+ self.improper_input_file_mapping[rel_name].append(rel_output_name)
+ self.proper_input_file_mapping[os.path.join(input_folder, rel_name)] = rel_output_name
+ self.proper_input_file_mapping[rel_output_name] = rel_output_name
+
def set_site(self, site):
site.register_path_handler('listing', self.listing_path)
+
+ # We need to prepare some things for the listings path handler to work.
+
+ self.kw = {
+ "default_lang": site.config["DEFAULT_LANG"],
+ "listings_folders": site.config["LISTINGS_FOLDERS"],
+ "output_folder": site.config["OUTPUT_FOLDER"],
+ "index_file": site.config["INDEX_FILE"],
+ "strip_indexes": site.config['STRIP_INDEXES'],
+ "filters": site.config["FILTERS"],
+ }
+
+ # Verify that no folder in LISTINGS_FOLDERS appears twice (on output side)
+ appearing_paths = set()
+ for source, dest in self.kw['listings_folders'].items():
+ if source in appearing_paths or dest in appearing_paths:
+ problem = source if source in appearing_paths else dest
+ utils.LOGGER.error("The listings input or output folder '{0}' appears in more than one entry in LISTINGS_FOLDERS, exiting.".format(problem))
+ sys.exit(1)
+ appearing_paths.add(source)
+ appearing_paths.add(dest)
+
+ # improper_input_file_mapping maps a relative input file (relative to
+ # its corresponding input directory) to a list of the output files.
+ # Since several input directories can contain files of the same name,
+ # a list is needed. This is needed for compatibility to previous Nikola
+ # versions, where there was no need to specify the input directory name
+ # when asking for a link via site.link('listing', ...).
+ self.improper_input_file_mapping = {}
+
+ # proper_input_file_mapping maps relative input file (relative to CWD)
+ # to a generated output file. Since we don't allow an input directory
+ # to appear more than once in LISTINGS_FOLDERS, we can map directly to
+ # a file name (and not a list of files).
+ self.proper_input_file_mapping = {}
+
+ for input_folder, output_folder in self.kw['listings_folders'].items():
+ for root, dirs, files in os.walk(input_folder, followlinks=True):
+ # Compute relative path; can't use os.path.relpath() here as it returns "." instead of ""
+ rel_path = root[len(input_folder):]
+ if rel_path[:1] == os.sep:
+ rel_path = rel_path[1:]
+
+ for f in files + [self.kw['index_file']]:
+ rel_name = os.path.join(rel_path, f)
+ rel_output_name = os.path.join(output_folder, rel_path, f)
+ # Register file names in the mapping.
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+
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", ".pyo")
- def render_listing(in_name, out_name, folders=[], files=[]):
+ def render_listing(in_name, out_name, input_folder, output_folder, 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, force=True),
- anchorlinenos=True))
- # the pygments highlighter uses <div class="codehilite"><pre>
- # for code. We switch it to reST's <pre class="code">.
- code = CODERE.sub('<pre class="code literal-block">\\1</pre>', code)
+ code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name))
title = os.path.basename(in_name)
else:
code = ''
- title = ''
+ title = os.path.split(os.path.dirname(out_name))[1]
crumbs = utils.get_crumbs(os.path.relpath(out_name,
- kw['output_folder']),
+ self.kw['output_folder']),
is_file=True)
permalink = self.site.link(
'listing',
- os.path.relpath(
- out_name,
- os.path.join(
- kw['output_folder'],
- kw['listings_folder'])))
- if self.site.config['COPY_SOURCES']:
- source_link = permalink[:-5]
+ os.path.join(
+ input_folder,
+ os.path.relpath(
+ out_name[:-5], # remove '.html'
+ os.path.join(
+ self.kw['output_folder'],
+ output_folder))))
+ if self.site.config['COPY_SOURCES'] and in_name:
+ source_link = permalink[:-5] # remove '.html'
else:
source_link = None
context = {
@@ -101,88 +142,121 @@ class Listings(Task):
'title': title,
'crumbs': crumbs,
'permalink': permalink,
- 'lang': kw['default_lang'],
- 'folders': natsort.natsorted(folders),
- 'files': natsort.natsorted(files),
+ 'lang': self.kw['default_lang'],
+ 'folders': natsort.natsorted(
+ folders, alg=natsort.ns.F | natsort.ns.IC),
+ 'files': natsort.natsorted(
+ files, alg=natsort.ns.F | natsort.ns.IC),
'description': title,
'source_link': source_link,
}
- self.site.render_template('listing.tmpl', out_name,
- context)
+ 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'], followlinks=True):
- files = [f for f in files if os.path.splitext(f)[-1] not in ignored_extensions]
-
- uptodate = {'c': self.site.GLOBAL_CONTEXT}
-
- for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- uptodate['||template_hooks|{0}||'.format(k)] = v._items
-
- for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
- uptodate[k] = self.site.GLOBAL_CONTEXT[k](kw['default_lang'])
-
- # save navigation links as dependencies
- uptodate['navigation_links'] = uptodate['c']['navigation_links'](kw['default_lang'])
-
- uptodate2 = uptodate.copy()
- uptodate2['f'] = files
- uptodate2['d'] = dirs
-
- # 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(uptodate2)],
- '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 {
+
+ for input_folder, output_folder in self.kw['listings_folders'].items():
+ for root, dirs, files in os.walk(input_folder, followlinks=True):
+ files = [f for f in files if os.path.splitext(f)[-1] not in ignored_extensions]
+
+ uptodate = {'c': self.site.GLOBAL_CONTEXT}
+
+ for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
+ uptodate['||template_hooks|{0}||'.format(k)] = v._items
+
+ for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
+ uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang'])
+
+ # save navigation links as dependencies
+ uptodate['navigation_links'] = uptodate['c']['navigation_links'](self.kw['default_lang'])
+
+ uptodate['kw'] = self.kw
+
+ uptodate2 = uptodate.copy()
+ uptodate2['f'] = files
+ uptodate2['d'] = dirs
+
+ # Compute relative path; can't use os.path.relpath() here as it returns "." instead of ""
+ rel_path = root[len(input_folder):]
+ if rel_path[:1] == os.sep:
+ rel_path = rel_path[1:]
+
+ rel_name = os.path.join(rel_path, self.kw['index_file'])
+ rel_output_name = os.path.join(output_folder, rel_path, self.kw['index_file'])
+
+ # Render all files
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ yield utils.apply_filters({
'basename': self.name,
'name': out_name,
- 'file_dep': template_deps + [in_name],
+ 'file_dep': template_deps,
'targets': [out_name],
- 'actions': [(render_listing, [in_name, out_name])],
+ 'actions': [(render_listing, [None, out_name, input_folder, output_folder, dirs, files])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
- 'uptodate': [utils.config_changed(uptodate)],
+ 'uptodate': [utils.config_changed(uptodate2, 'nikola.plugins.task.listings:folder')],
'clean': True,
- }
- if self.site.config['COPY_SOURCES']:
- out_name = os.path.join(
- kw['output_folder'],
- root,
- f)
- yield {
+ }, self.kw["filters"])
+ for f in files:
+ ext = os.path.splitext(f)[-1]
+ if ext in ignored_extensions:
+ continue
+ in_name = os.path.join(root, f)
+ # Record file names
+ rel_name = os.path.join(rel_path, f + '.html')
+ rel_output_name = os.path.join(output_folder, rel_path, f + '.html')
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+ # Set up output name
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ # Yield task
+ yield utils.apply_filters({
'basename': self.name,
'name': out_name,
- 'file_dep': [in_name],
+ 'file_dep': template_deps + [in_name],
'targets': [out_name],
- 'actions': [(utils.copy_file, [in_name, out_name])],
+ 'actions': [(render_listing, [in_name, out_name, input_folder, output_folder])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(uptodate, 'nikola.plugins.task.listings:source')],
'clean': True,
- }
+ }, self.kw["filters"])
+ if self.site.config['COPY_SOURCES']:
+ rel_name = os.path.join(rel_path, f)
+ rel_output_name = os.path.join(output_folder, rel_path, f)
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': out_name,
+ 'file_dep': [in_name],
+ 'targets': [out_name],
+ 'actions': [(utils.copy_file, [in_name, out_name])],
+ 'clean': True,
+ }, self.kw["filters"])
- def listing_path(self, name, lang):
- if not name.endswith('.html'):
+ def listing_path(self, namep, lang):
+ namep = namep.replace('/', os.sep)
+ nameh = namep + '.html'
+ for name in (namep, nameh):
+ if name in self.proper_input_file_mapping:
+ # If the name shows up in this dict, everything's fine.
+ name = self.proper_input_file_mapping[name]
+ break
+ elif name in self.improper_input_file_mapping:
+ # If the name shows up in this dict, we have to check for
+ # ambiguities.
+ if len(self.improper_input_file_mapping[name]) > 1:
+ utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name])))
+ sys.exit(1)
+ if len(self.site.config['LISTINGS_FOLDERS']) > 1:
+ utils.LOGGER.notice("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
+ name = self.improper_input_file_mapping[name][0]
+ break
+ else:
+ utils.LOGGER.error("Unknown listing name {0}!".format(namep))
+ sys.exit(1)
+ if not name.endswith(os.sep + self.site.config["INDEX_FILE"]):
name += '.html'
- path_parts = [self.site.config['LISTINGS_FOLDER']] + list(os.path.split(name))
+ path_parts = name.split(os.sep)
return [_f for _f in path_parts if _f]
diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin
index 67212d2..4cad7b7 100644
--- a/nikola/plugins/task/pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -4,7 +4,7 @@ Module = pages
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create pages in the output.
diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py
index aefc5a1..d0edb56 100644
--- a/nikola/plugins/task/pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,9 +51,7 @@ class RenderPages(Task):
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['uptodate'] = task['uptodate'] + [config_changed(kw, 'nikola.plugins.task.pages')]
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
index e1a42fd..707b3c2 100644
--- a/nikola/plugins/task/posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -4,7 +4,7 @@ Module = posts
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
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
index 8e03122..d3f17fd 100644
--- a/nikola/plugins/task/posts.py
+++ b/nikola/plugins/task/posts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,18 +25,20 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from copy import copy
+import os
from nikola.plugin_categories import Task
-from nikola import utils
+from nikola import filters, utils
-def rest_deps(post, task):
- """Add extra_deps from ReST into task.
+def update_deps(post, lang, task):
+ """Updates file dependencies as they might have been updated during compilation.
- The .dep file is created by ReST so not available before the task starts
- to execute.
+ This is done for example by the ReST page compiler, which writes its
+ dependencies into a .dep file. This file is read and incorporated when calling
+ post.fragment_deps(), and only available /after/ compiling the fragment.
"""
- task.file_dep.update(post.extra_deps())
+ task.file_dep.update([p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")])
class RenderPosts(Task):
@@ -54,23 +56,62 @@ class RenderPosts(Task):
"show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
"demote_headers": self.site.config['DEMOTE_HEADERS'],
}
+ self.tl_changed = False
yield self.group_task()
+ def tl_ch():
+ self.tl_changed = True
+
+ yield {
+ 'basename': self.name,
+ 'name': 'timeline_changes',
+ 'actions': [tl_ch],
+ 'uptodate': [utils.config_changed({1: kw['timeline']})],
+ }
+
for lang in kw["translations"]:
deps_dict = copy(kw)
deps_dict.pop('timeline')
for post in kw['timeline']:
+
dest = post.translated_base_path(lang)
+ file_dep = [p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")]
task = {
'basename': self.name,
'name': dest,
- 'file_dep': post.fragment_deps(lang),
+ 'file_dep': file_dep,
'targets': [dest],
'actions': [(post.compile, (lang, )),
- (rest_deps, (post,)),
+ (update_deps, (post, lang, )),
],
'clean': True,
- 'uptodate': [utils.config_changed(deps_dict)],
+ 'uptodate': [
+ utils.config_changed(deps_dict, 'nikola.plugins.task.posts'),
+ lambda p=post, l=lang: self.dependence_on_timeline(p, l)
+ ] + post.fragment_deps_uptodate(lang),
+ 'task_dep': ['render_posts:timeline_changes']
}
- yield task
+
+ # Apply filters specified in the metadata
+ ff = [x.strip() for x in post.meta('filters', lang).split(',')]
+ flist = []
+ for i, f in enumerate(ff):
+ if not f:
+ continue
+ if f.startswith('filters.'): # A function from the filters module
+ f = f[8:]
+ try:
+ flist.append(getattr(filters, f))
+ except AttributeError:
+ pass
+ else:
+ flist.append(f)
+ yield utils.apply_filters(task, {os.path.splitext(dest): flist})
+
+ def dependence_on_timeline(self, post, lang):
+ if "####MAGIC####TIMELINE" not in post.fragment_deps(lang):
+ return True # No dependency on timeline
+ elif self.tl_changed:
+ return False # Timeline changed
+ return True
diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin
index 826f3d8..0228c70 100644
--- a/nikola/plugins/task/redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -4,7 +4,7 @@ Module = redirect
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create redirect pages.
diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py
index e1134bf..428dd5a 100644
--- a/nikola/plugins/task/redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,7 +24,8 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import io
+from __future__ import unicode_literals
+
import os
from nikola.plugin_categories import Task
@@ -42,26 +43,18 @@ class Redirect(Task):
kw = {
'redirections': self.site.config['REDIRECTIONS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
}
yield self.group_task()
if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src)
- yield {
+ yield utils.apply_filters({
'basename': self.name,
'name': src_path,
'targets': [src_path],
- 'actions': [(create_redirect, (src_path, dst))],
+ 'actions': [(utils.create_redirect, (src_path, dst))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
-
-def create_redirect(src, dst):
- utils.makedirs(os.path.dirname(src))
- with io.open(src, "w+", encoding="utf8") as fd:
- fd.write('<!DOCTYPE html><head><title>Redirecting...</title>'
- '<meta name="robots" content="noindex">'
- '<meta http-equiv="refresh" content="0; '
- 'url={0}"></head><body><p>Page moved <a href="{0}">here</a></p></body>'.format(dst))
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.redirect')],
+ }, kw["filters"])
diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin
index 60b50fb..b4b43a3 100644
--- a/nikola/plugins/task/robots.plugin
+++ b/nikola/plugins/task/robots.plugin
@@ -4,7 +4,7 @@ Module = robots
[Documentation]
Author = Daniel Aleksandersen
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate /robots.txt exclusion file and promote sitemap.
diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py
index b229d37..2f25a21 100644
--- a/nikola/plugins/task/robots.py
+++ b/nikola/plugins/task/robots.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -48,7 +48,8 @@ class RobotsFile(LateTask):
"site_url": self.site.config["SITE_URL"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"files_folders": self.site.config['FILES_FOLDERS'],
- "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"]
+ "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
+ "filters": self.site.config["FILTERS"],
}
sitemapindex_url = urljoin(kw["base_url"], "sitemapindex.xml")
@@ -68,15 +69,15 @@ class RobotsFile(LateTask):
yield self.group_task()
if not utils.get_asset_path("robots.txt", [], files_folders=kw["files_folders"]):
- yield {
+ yield utils.apply_filters({
"basename": self.name,
"name": robots_path,
"targets": [robots_path],
"actions": [(write_robots)],
- "uptodate": [utils.config_changed(kw)],
+ "uptodate": [utils.config_changed(kw, 'nikola.plugins.task.robots')],
"clean": True,
"task_dep": ["sitemap"]
- }
+ }, kw["filters"])
elif kw["robots_exclusions"]:
utils.LOGGER.warn('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied fie.')
else:
diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin
index 7206a43..56f0bf4 100644
--- a/nikola/plugins/task/rss.plugin
+++ b/nikola/plugins/task/rss.plugin
@@ -4,7 +4,7 @@ Module = rss
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate RSS feeds.
diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py
index b16ed48..26a4da1 100644
--- a/nikola/plugins/task/rss.py
+++ b/nikola/plugins/task/rss.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,6 +51,7 @@ class GenerateRSS(Task):
"filters": self.site.config["FILTERS"],
"blog_title": self.site.config["BLOG_TITLE"],
"site_url": self.site.config["SITE_URL"],
+ "base_url": self.site.config["BASE_URL"],
"blog_description": self.site.config["BLOG_DESCRIPTION"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
@@ -59,6 +60,7 @@ class GenerateRSS(Task):
"feed_length": self.site.config['FEED_LENGTH'],
"tzinfo": self.site.tzinfo,
"rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"],
+ "rss_links_append_query": self.site.config["RSS_LINKS_APPEND_QUERY"],
}
self.site.scan_posts()
# Check for any changes in the state of use_in_feeds for any post.
@@ -71,16 +73,18 @@ class GenerateRSS(Task):
output_name = os.path.join(kw['output_folder'],
self.site.path("rss", None, lang))
deps = []
+ deps_uptodate = []
if kw["show_untranslated_posts"]:
- posts = self.site.posts[:10]
+ posts = self.site.posts[:kw['feed_length']]
else:
- posts = [x for x in self.site.posts if x.is_translation_available(lang)][:10]
+ posts = [x for x in self.site.posts if x.is_translation_available(lang)][:kw['feed_length']]
for post in posts:
deps += post.deps(lang)
+ deps_uptodate += post.deps_uptodate(lang)
feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/'))
- yield {
+ task = {
'basename': 'generate_rss',
'name': os.path.normpath(output_name),
'file_dep': deps,
@@ -88,12 +92,14 @@ class GenerateRSS(Task):
'actions': [(utils.generic_rss_renderer,
(lang, kw["blog_title"](lang), kw["site_url"],
kw["blog_description"](lang), posts, output_name,
- kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], feed_url))],
+ kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], feed_url,
+ None, kw["rss_links_append_query"]))],
'task_dep': ['render_posts'],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.rss')] + deps_uptodate,
}
+ yield utils.apply_filters(task, kw['filters'])
def rss_path(self, name, lang):
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
diff --git a/nikola/plugins/task/scale_images.plugin b/nikola/plugins/task/scale_images.plugin
new file mode 100644
index 0000000..c0f0f28
--- /dev/null
+++ b/nikola/plugins/task/scale_images.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = scale_images
+Module = scale_images
+
+[Documentation]
+Author = Pelle Nilsson
+Version = 1.0
+Website = http://getnikola.com
+Description = Create down-scaled images and thumbnails.
diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py
new file mode 100644
index 0000000..f97027e
--- /dev/null
+++ b/nikola/plugins/task/scale_images.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2014-2015 Pelle Nilsson 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.image_processing import ImageProcessor
+from nikola import utils
+
+
+class ScaleImage(Task, ImageProcessor):
+ """Copy static files into the output folder."""
+
+ name = "scale_images"
+
+ def set_site(self, site):
+ self.logger = utils.get_logger('scale_images', site.loghandlers)
+ return super(ScaleImage, self).set_site(site)
+
+ def process_tree(self, src, dst):
+ """Processes all images in a src tree and put the (possibly) rescaled
+ images in the dst folder."""
+ ignore = set(['.svn'])
+ base_len = len(src.split(os.sep))
+ for root, dirs, files in os.walk(src, followlinks=True):
+ root_parts = root.split(os.sep)
+ if set(root_parts) & ignore:
+ continue
+ dst_dir = os.path.join(dst, *root_parts[base_len:])
+ utils.makedirs(dst_dir)
+ for src_name in files:
+ if src_name in ('.DS_Store', 'Thumbs.db'):
+ continue
+ if (not src_name.lower().endswith(tuple(self.image_ext_list)) and not src_name.upper().endswith(tuple(self.image_ext_list))):
+ continue
+ dst_file = os.path.join(dst_dir, src_name)
+ src_file = os.path.join(root, src_name)
+ thumb_file = '.thumbnail'.join(os.path.splitext(dst_file))
+ yield {
+ 'name': dst_file,
+ 'file_dep': [src_file],
+ 'targets': [dst_file, thumb_file],
+ 'actions': [(self.process_image, (src_file, dst_file, thumb_file))],
+ 'clean': True,
+ }
+
+ def process_image(self, src, dst, thumb):
+ self.resize_image(src, dst, self.kw['max_image_size'], False)
+ self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False)
+
+ def gen_tasks(self):
+ """Copy static files into the output folder."""
+
+ self.kw = {
+ 'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'image_folders': self.site.config['IMAGE_FOLDERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
+ }
+
+ self.image_ext_list = self.image_ext_list_builtin
+ self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
+
+ yield self.group_task()
+ for src in self.kw['image_folders']:
+ dst = self.kw['output_folder']
+ filters = self.kw['filters']
+ real_dst = os.path.join(dst, self.kw['image_folders'][src])
+ for task in self.process_tree(src, real_dst):
+ task['basename'] = self.name
+ task['uptodate'] = [utils.config_changed(self.kw)]
+ yield utils.apply_filters(task, filters)
diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index 2cd8195..0b992b8 100644
--- a/nikola/plugins/task/sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -4,7 +4,7 @@ Module = sitemap
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate google sitemap.
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py
index 943e9b2..92d557d 100644
--- a/nikola/plugins/task/sitemap/__init__.py
+++ b/nikola/plugins/task/sitemap/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -36,7 +36,7 @@ except ImportError:
import urllib.robotparser as robotparser # NOQA
from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed
+from nikola.utils import config_changed, apply_filters
urlset_header = """<?xml version="1.0" encoding="UTF-8"?>
@@ -49,7 +49,7 @@ urlset_header = """<?xml version="1.0" encoding="UTF-8"?>
loc_format = """ <url>
<loc>{0}</loc>
- <lastmod>{1}</lastmod>
+ <lastmod>{1}</lastmod>{2}
</url>
"""
@@ -69,6 +69,9 @@ sitemap_format = """ <sitemap>
</sitemap>
"""
+alternates_format = """\n <xhtml:link rel="alternate" hreflang="{0}" href="{1}" />"""
+
+
sitemapindex_footer = "</sitemapindex>"
@@ -111,8 +114,10 @@ class Sitemap(LateTask):
"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', '.rss']),
- "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"]
+ "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.xml', '.rss']),
+ "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
+ "filters": self.site.config["FILTERS"],
+ "translations": self.site.config["TRANSLATIONS"],
}
output = kw['output_folder']
@@ -136,7 +141,17 @@ class Sitemap(LateTask):
lastmod = self.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
- urlset[loc] = loc_format.format(loc, lastmod)
+ post = self.site.post_per_file.get(path + kw['index_file'])
+ if post and (post.is_draft or post.is_private or post.publish_later):
+ continue
+ alternates = []
+ if post:
+ for lang in kw['translations']:
+ alt_url = post.permalink(lang=lang, absolute=True)
+ if loc == alt_url:
+ continue
+ alternates.append(alternates_format.format(lang, alt_url))
+ urlset[loc] = loc_format.format(loc, lastmod, ''.join(alternates))
for fname in files:
if kw['strip_indexes'] and fname == kw['index_file']:
continue # We already mapped the folder
@@ -148,20 +163,30 @@ class Sitemap(LateTask):
continue
if not robot_fetch(path):
continue
+
+ # read in binary mode to make ancient files work
+ fh = open(real_path, 'rb')
+ filehead = fh.read(1024)
+ fh.close()
+
if path.endswith('.html') or path.endswith('.htm'):
- try:
- if u'<!doctype html' not in io.open(real_path, 'r', encoding='utf8').read(1024).lower():
- # ignores "html" files without doctype
- # alexa-verify, google-site-verification, etc.
- continue
- except UnicodeDecodeError:
- # ignore ancient files
- # most non-utf8 files are worthless anyways
+ """ ignores "html" files without doctype """
+ if b'<!doctype html' not in filehead.lower():
continue
- """ put RSS in sitemapindex[] instead of in urlset[], sitemap_path is included after it is generated """
- if path.endswith('.xml') or path.endswith('.rss'):
- filehead = io.open(real_path, 'r', encoding='utf8').read(512)
- if u'<rss' in filehead or (u'<urlset' in filehead and path != sitemap_path):
+
+ """ ignores "html" files with noindex robot directives """
+ robots_directives = [b'<meta content="noindex" name="robots"',
+ b'<meta content="none" name="robots"',
+ b'<meta name="robots" content="noindex"',
+ b'<meta name="robots" content="none"']
+ if any([robot_directive in filehead.lower() for robot_directive in robots_directives]):
+ continue
+
+ # put Atom and RSS in sitemapindex[] instead of in urlset[],
+ # sitemap_path is included after it is generated
+ if path.endswith('.xml') or path.endswith('.atom') or path.endswith('.rss'):
+ known_elm_roots = (b'<feed', b'<rss', b'<urlset')
+ if any([elm_root in filehead.lower() for elm_root in known_elm_roots]) and path != sitemap_path:
path = path.replace(os.sep, '/')
lastmod = self.get_lastmod(real_path)
loc = urljoin(base_url, base_path + path)
@@ -175,7 +200,14 @@ class Sitemap(LateTask):
path = path.replace(os.sep, '/')
lastmod = self.get_lastmod(real_path)
loc = urljoin(base_url, base_path + path)
- urlset[loc] = loc_format.format(loc, lastmod)
+ alternates = []
+ if post:
+ for lang in kw['translations']:
+ alt_url = post.permalink(lang=lang, absolute=True)
+ if loc == alt_url:
+ continue
+ alternates.append(alternates_format.format(lang, alt_url))
+ urlset[loc] = loc_format.format(loc, lastmod, '\n'.join(alternates))
def robot_fetch(path):
for rule in kw["robots_exclusions"]:
@@ -208,7 +240,27 @@ class Sitemap(LateTask):
# to scan locations.
def scan_locs_task():
scan_locs()
- return {'locations': list(urlset.keys()) + list(sitemapindex.keys())}
+
+ # Generate a list of file dependencies for the actual generation
+ # task, so rebuilds are triggered. (Issue #1032)
+ output = kw["output_folder"]
+ file_dep = []
+
+ for i in urlset.keys():
+ p = os.path.join(output, urlparse(i).path.replace(base_path, '', 1))
+ if not p.endswith('sitemap.xml') and not os.path.isdir(p):
+ file_dep.append(p)
+ if os.path.isdir(p) and os.path.exists(os.path.join(p, 'index.html')):
+ file_dep.append(p + 'index.html')
+
+ for i in sitemapindex.keys():
+ p = os.path.join(output, urlparse(i).path.replace(base_path, '', 1))
+ if not p.endswith('sitemap.xml') and not os.path.isdir(p):
+ file_dep.append(p)
+ if os.path.isdir(p) and os.path.exists(os.path.join(p, 'index.html')):
+ file_dep.append(p + 'index.html')
+
+ return {'file_dep': file_dep}
yield {
"basename": "_scan_locs",
@@ -217,29 +269,29 @@ class Sitemap(LateTask):
}
yield self.group_task()
- yield {
+ yield apply_filters({
"basename": "sitemap",
"name": sitemap_path,
"targets": [sitemap_path],
"actions": [(write_sitemap,)],
- "uptodate": [config_changed(kw)],
+ "uptodate": [config_changed(kw, 'nikola.plugins.task.sitemap:write')],
"clean": True,
"task_dep": ["render_site"],
"calc_dep": ["_scan_locs:sitemap"],
- }
- yield {
+ }, kw['filters'])
+ yield apply_filters({
"basename": "sitemap",
"name": sitemapindex_path,
"targets": [sitemapindex_path],
"actions": [(write_sitemapindex,)],
- "uptodate": [config_changed(kw)],
+ "uptodate": [config_changed(kw, 'nikola.plugins.task.sitemap:write_index')],
"clean": True,
"file_dep": [sitemap_path]
- }
+ }, kw['filters'])
def get_lastmod(self, p):
if self.site.invariant:
- return '2014-01-01'
+ return '2038-01-01'
else:
return datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin
index 6224e48..5560df6 100644
--- a/nikola/plugins/task/sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -4,7 +4,7 @@ Module = sources
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
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
index 4c669c2..840a31c 100644
--- a/nikola/plugins/task/sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -49,6 +49,7 @@ class Sources(Task):
"translations": self.site.config["TRANSLATIONS"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"default_lang": self.site.config["DEFAULT_LANG"],
+ "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
@@ -56,6 +57,8 @@ class Sources(Task):
if self.site.config['COPY_SOURCES']:
for lang in kw["translations"]:
for post in self.site.timeline:
+ if not kw["show_untranslated_posts"] and lang not in post.translated_to:
+ continue
if post.meta('password'):
continue
output_name = os.path.join(
@@ -77,5 +80,5 @@ class Sources(Task):
'targets': [output_name],
'actions': [(utils.copy_file, (source, output_name))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.sources')],
}
diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin
index f01e0f8..4ac3800 100644
--- a/nikola/plugins/task/tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -4,7 +4,7 @@ Module = tags
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
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
index 8d43f13..832ceff 100644
--- a/nikola/plugins/task/tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,6 +27,8 @@
from __future__ import unicode_literals
import json
import os
+import sys
+import natsort
try:
from urlparse import urljoin
except ImportError:
@@ -43,9 +45,12 @@ class RenderTags(Task):
def set_site(self, site):
site.register_path_handler('tag_index', self.tag_index_path)
+ site.register_path_handler('category_index', self.category_index_path)
site.register_path_handler('tag', self.tag_path)
+ site.register_path_handler('tag_atom', self.tag_atom_path)
site.register_path_handler('tag_rss', self.tag_rss_path)
site.register_path_handler('category', self.category_path)
+ site.register_path_handler('category_atom', self.category_atom_path)
site.register_path_handler('category_rss', self.category_rss_path)
return super(RenderTags, self).set_site(site)
@@ -56,18 +61,26 @@ class RenderTags(Task):
"translations": self.site.config["TRANSLATIONS"],
"blog_title": self.site.config["BLOG_TITLE"],
"site_url": self.site.config["SITE_URL"],
+ "base_url": self.site.config["BASE_URL"],
"messages": self.site.MESSAGES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ 'tag_path': self.site.config['TAG_PATH'],
"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'],
+ 'category_path': self.site.config['CATEGORY_PATH'],
+ 'category_prefix': self.site.config['CATEGORY_PREFIX'],
+ "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
"generate_rss": self.site.config['GENERATE_RSS'],
"rss_teasers": self.site.config["RSS_TEASERS"],
"rss_plain": self.site.config["RSS_PLAIN"],
+ "rss_link_append_query": self.site.config["RSS_LINKS_APPEND_QUERY"],
"show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
"feed_length": self.site.config['FEED_LENGTH'],
+ "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
"tzinfo": self.site.tzinfo,
+ "pretty_urls": self.site.config['PRETTY_URLS'],
+ "strip_indexes": self.site.config['STRIP_INDEXES'],
+ "index_file": self.site.config['INDEX_FILE'],
}
self.site.scan_posts()
@@ -78,6 +91,32 @@ class RenderTags(Task):
if not self.site.posts_per_tag and not self.site.posts_per_category:
return
+ if kw['category_path'] == kw['tag_path']:
+ tags = {self.slugify_tag_name(tag): tag for tag in self.site.posts_per_tag.keys()}
+ cats = {tuple(self.slugify_category_name(category)): category for category in self.site.posts_per_category.keys()}
+ categories = {k[0]: v for k, v in cats.items() if len(k) == 1}
+ intersect = set(tags.keys()) & set(categories.keys())
+ if len(intersect) > 0:
+ for slug in intersect:
+ utils.LOGGER.error("Category '{0}' and tag '{1}' both have the same slug '{2}'!".format('/'.join(categories[slug]), tags[slug], slug))
+ sys.exit(1)
+
+ # Test for category slug clashes
+ categories = {}
+ for category in self.site.posts_per_category.keys():
+ slug = tuple(self.slugify_category_name(category))
+ for part in slug:
+ if len(part) == 0:
+ utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
+ sys.exit(1)
+ if slug in categories:
+ other_category = categories[slug]
+ utils.LOGGER.error('You have categories that are too similar: {0} and {1}'.format(category, other_category))
+ utils.LOGGER.error('Category {0} is used in: {1}'.format(category, ', '.join([p.source_path for p in self.site.posts_per_category[category]])))
+ utils.LOGGER.error('Category {0} is used in: {1}'.format(other_category, ', '.join([p.source_path for p in self.site.posts_per_category[other_category]])))
+ sys.exit(1)
+ categories[slug] = category
+
tag_list = list(self.site.posts_per_tag.items())
cat_list = list(self.site.posts_per_category.items())
@@ -92,7 +131,7 @@ class RenderTags(Task):
if kw["generate_rss"]:
yield self.tag_rss(tag, lang, filtered_posts, kw, is_category)
# Render HTML
- if kw['tag_pages_are_indexes']:
+ if kw['category_pages_are_indexes'] if is_category else 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)
@@ -101,19 +140,19 @@ class RenderTags(Task):
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):
+ for path, posts in cat_list:
+ for task in render_lists(path, posts, True):
yield task
# Tag cloud json file
tag_cloud_data = {}
for tag, posts in self.site.posts_per_tag.items():
+ if tag in self.site.config['HIDDEN_TAGS']:
+ continue
tag_posts = dict(posts=[{'title': post.meta[post.default_lang]['title'],
'date': post.date.strftime('%m/%d/%Y'),
'isodate': post.date.isoformat(),
- 'url': post.base_path.replace('cache', '')}
+ 'url': post.permalink(post.default_lang)}
for post in reversed(sorted(self.site.timeline, key=lambda post: post.date))
if tag in post.alltags])
tag_cloud_data[tag] = [len(posts), self.site.link(
@@ -126,48 +165,59 @@ class RenderTags(Task):
with open(output_name, 'w+') as fd:
json.dump(data, fd)
- task = {
- 'basename': str(self.name),
- 'name': str(output_name)
- }
+ if self.site.config['WRITE_TAG_CLOUD']:
+ 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
+ task['uptodate'] = [utils.config_changed(tag_cloud_data, 'nikola.plugins.task.tags:tagdata')]
+ task['targets'] = [output_name]
+ task['actions'] = [(write_tag_data, [tag_cloud_data])]
+ task['clean'] = True
+ yield utils.apply_filters(task, kw['filters'])
- def list_tags_page(self, kw):
+ def _create_tags_page(self, kw, include_tags=True, include_categories=True):
"""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
+ tags = natsort.natsorted([tag for tag in self.site.posts_per_tag.keys()
+ if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]],
+ alg=natsort.ns.F | natsort.ns.IC)
+ categories = [cat.category_name for cat in self.site.category_hierarchy]
+ has_tags = (tags != []) and include_tags
+ has_categories = (categories != []) and include_categories
template_name = "tags.tmpl"
- kw['tags'] = tags
- kw['categories'] = categories
+ kw = kw.copy()
+ if include_tags:
+ kw['tags'] = tags
+ if include_categories:
+ kw['categories'] = categories
for lang in kw["translations"]:
output_name = os.path.join(
- kw['output_folder'], self.site.path('tag_index', None, lang))
+ kw['output_folder'], self.site.path('tag_index' if has_tags else 'category_index', None, lang))
output_name = output_name
context = {}
- if has_categories:
+ if has_categories and has_tags:
context["title"] = kw["messages"][lang]["Tags and Categories"]
+ elif has_categories:
+ context["title"] = kw["messages"][lang]["Categories"]
else:
context["title"] = kw["messages"][lang]["Tags"]
- context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
- in tags]
+ if has_tags:
+ context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
+ in tags]
+ else:
+ context["items"] = None
if has_categories:
context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag
in categories]
+ context['cat_hierarchy'] = [(node.name, node.category_name, node.category_path,
+ self.site.link("category", node.category_name),
+ node.indent_levels, node.indent_change_before,
+ node.indent_change_after)
+ for node in self.site.category_hierarchy]
else:
context["cat_items"] = None
- context["permalink"] = self.site.link("tag_index", None, lang)
+ context["permalink"] = self.site.link("tag_index" if has_tags else "category_index", None, lang)
context["description"] = context["title"]
task = self.site.generic_post_list_renderer(
lang,
@@ -177,73 +227,66 @@ class RenderTags(Task):
kw['filters'],
context,
)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [utils.config_changed(task_cfg)]
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:page')]
task['basename'] = str(self.name)
yield task
+ def list_tags_page(self, kw):
+ """a global "all your tags/categories" page for each language"""
+ if self.site.config['TAG_PATH'] == self.site.config['CATEGORY_PATH']:
+ yield self._create_tags_page(kw, True, True)
+ else:
+ yield self._create_tags_page(kw, False, True)
+ yield self._create_tags_page(kw, True, False)
+
+ def _get_title(self, tag, is_category):
+ if is_category:
+ return self.site.parse_category_name(tag)[-1]
+ else:
+ return tag
+
+ def _get_description(self, tag, is_category, lang):
+ descriptions = self.site.config['CATEGORY_PAGES_DESCRIPTIONS'] if is_category else self.site.config['TAG_PAGES_DESCRIPTIONS']
+ return descriptions[lang][tag] if lang in descriptions and tag in descriptions[lang] else None
+
+ def _get_subcategories(self, category):
+ node = self.site.category_hierarchy_lookup[category]
+ return [(child.name, self.site.link("category", child.category_name)) for child in node.children]
+
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
+ def page_link(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return utils.adjust_name_for_index_link(self.site.link(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension)
+
+ def page_path(i, displayed_i, num_pages, force_addition, extension=None):
+ feed = "_atom" if extension == ".atom" else ""
+ return utils.adjust_name_for_index_path(self.site.path(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension)
+
+ context_source = {}
+ title = self._get_title(tag, is_category)
+ if kw["generate_rss"]:
+ # 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(
+ title, lang, self.site.link(kind + "_rss", tag, lang)))
+ context_source['rss_link'] = rss_link
+ if is_category:
+ context_source["category"] = tag
+ context_source["category_path"] = self.site.parse_category_name(tag)
+ context_source["tag"] = title
+ indexes_title = kw["messages"][lang]["Posts about %s"] % title
+ context_source["description"] = self._get_description(tag, is_category, lang)
+ if is_category:
+ context_source["subcategories"] = self._get_subcategories(tag)
template_name = "tagindex.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 = {}
- if kw["generate_rss"]:
- # 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"] = context["title"]
- 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
+ yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path)
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"""
@@ -253,12 +296,18 @@ class RenderTags(Task):
kind, tag, lang))
context = {}
context["lang"] = lang
- context["title"] = kw["messages"][lang]["Posts about %s"] % tag
+ title = self._get_title(tag, is_category)
+ if is_category:
+ context["category"] = tag
+ context["category_path"] = self.site.parse_category_name(tag)
+ context["tag"] = title
+ context["title"] = kw["messages"][lang]["Posts about %s"] % title
context["posts"] = post_list
context["permalink"] = self.site.link(kind, tag, lang)
- context["tag"] = tag
context["kind"] = kind
- context["description"] = context["title"]
+ context["description"] = self._get_description(tag, is_category, lang)
+ if is_category:
+ context["subcategories"] = self._get_subcategories(tag)
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -267,8 +316,7 @@ class RenderTags(Task):
kw['filters'],
context,
)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [utils.config_changed(task_cfg)]
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')]
task['basename'] = str(self.name)
yield task
@@ -281,26 +329,29 @@ class RenderTags(Task):
self.site.path(kind + "_rss", tag, lang)))
feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/'))
deps = []
+ deps_uptodate = []
post_list = sorted(posts, key=lambda a: a.date)
post_list.reverse()
for post in post_list:
deps += post.deps(lang)
- return {
+ deps_uptodate += post.deps_uptodate(lang)
+ task = {
'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"](lang), tag),
+ (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, is_category)),
kw["site_url"], None, post_list,
output_name, kw["rss_teasers"], kw["rss_plain"], kw['feed_length'],
- feed_url))],
+ feed_url, None, kw["rss_link_append_query"]))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate,
'task_dep': ['render_posts'],
}
+ return utils.apply_filters(task, kw['filters'])
- def slugify_name(self, name):
+ def slugify_tag_name(self, name):
if self.site.config['SLUG_TAG_PATH']:
name = utils.slugify(name)
return name
@@ -310,30 +361,64 @@ class RenderTags(Task):
self.site.config['TAG_PATH'],
self.site.config['INDEX_FILE']] if _f]
+ def category_index_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH'],
+ self.site.config['INDEX_FILE']] if _f]
+
def tag_path(self, name, lang):
if self.site.config['PRETTY_URLS']:
return [_f for _f in [
self.site.config['TRANSLATIONS'][lang],
self.site.config['TAG_PATH'],
- self.slugify_name(name),
+ self.slugify_tag_name(name),
self.site.config['INDEX_FILE']] if _f]
else:
return [_f for _f in [
self.site.config['TRANSLATIONS'][lang],
self.site.config['TAG_PATH'],
- self.slugify_name(name) + ".html"] if _f]
+ self.slugify_tag_name(name) + ".html"] if _f]
+
+ def tag_atom_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".atom"] 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
+ self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".xml"] if
_f]
+ def slugify_category_name(self, name):
+ path = self.site.parse_category_name(name)
+ if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']:
+ path = path[-1:] # only the leaf
+ result = [self.slugify_tag_name(part) for part in path]
+ result[0] = self.site.config['CATEGORY_PREFIX'] + result[0]
+ if not self.site.config['PRETTY_URLS']:
+ result = ['-'.join(result)]
+ return result
+
+ def _add_extension(self, path, extension):
+ path[-1] += extension
+ return path
+
def category_path(self, name, lang):
+ if self.site.config['PRETTY_URLS']:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self.slugify_category_name(name) + [self.site.config['INDEX_FILE']]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".html")
+
+ def category_atom_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]
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".atom")
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]
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".xml")