aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/plugins/task')
-rw-r--r--nikola/plugins/task/__init__.py2
-rw-r--r--nikola/plugins/task/archive.plugin4
-rw-r--r--nikola/plugins/task/archive.py409
-rw-r--r--nikola/plugins/task/authors.plugin4
-rw-r--r--nikola/plugins/task/authors.py387
-rw-r--r--nikola/plugins/task/bundles.plugin4
-rw-r--r--nikola/plugins/task/bundles.py83
-rw-r--r--nikola/plugins/task/categories.plugin12
-rw-r--r--nikola/plugins/task/categories.py248
-rw-r--r--nikola/plugins/task/copy_assets.plugin2
-rw-r--r--nikola/plugins/task/copy_assets.py37
-rw-r--r--nikola/plugins/task/copy_files.plugin2
-rw-r--r--nikola/plugins/task/copy_files.py2
-rw-r--r--nikola/plugins/task/galleries.plugin2
-rw-r--r--nikola/plugins/task/galleries.py233
-rw-r--r--nikola/plugins/task/gzip.plugin2
-rw-r--r--nikola/plugins/task/gzip.py2
-rw-r--r--nikola/plugins/task/indexes.plugin5
-rw-r--r--nikola/plugins/task/indexes.py397
-rw-r--r--nikola/plugins/task/listings.plugin2
-rw-r--r--nikola/plugins/task/listings.py48
-rw-r--r--nikola/plugins/task/page_index.plugin12
-rw-r--r--nikola/plugins/task/page_index.py111
-rw-r--r--nikola/plugins/task/pages.plugin2
-rw-r--r--nikola/plugins/task/pages.py20
-rw-r--r--nikola/plugins/task/posts.plugin2
-rw-r--r--nikola/plugins/task/posts.py18
-rw-r--r--nikola/plugins/task/py3_switch.plugin13
-rw-r--r--nikola/plugins/task/py3_switch.py103
-rw-r--r--nikola/plugins/task/redirect.plugin2
-rw-r--r--nikola/plugins/task/redirect.py6
-rw-r--r--nikola/plugins/task/robots.plugin2
-rw-r--r--nikola/plugins/task/robots.py13
-rw-r--r--nikola/plugins/task/rss.plugin13
-rw-r--r--nikola/plugins/task/rss.py117
-rw-r--r--nikola/plugins/task/scale_images.plugin2
-rw-r--r--nikola/plugins/task/scale_images.py32
-rw-r--r--nikola/plugins/task/sitemap.plugin2
-rw-r--r--nikola/plugins/task/sitemap.py (renamed from nikola/plugins/task/sitemap/__init__.py)47
-rw-r--r--nikola/plugins/task/sources.plugin2
-rw-r--r--nikola/plugins/task/sources.py10
-rw-r--r--nikola/plugins/task/tags.plugin5
-rw-r--r--nikola/plugins/task/tags.py570
-rw-r--r--nikola/plugins/task/taxonomies.plugin12
-rw-r--r--nikola/plugins/task/taxonomies.py459
45 files changed, 1717 insertions, 1745 deletions
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
index 4eeae62..3e18cd5 100644
--- a/nikola/plugins/task/__init__.py
+++ b/nikola/plugins/task/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 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 eb079da..62e5fd9 100644
--- a/nikola/plugins/task/archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_archive
+name = classify_archive
module = archive
[Documentation]
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generates the blog's archive pages.
[Nikola]
-plugincategory = Task
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py
index 303d349..4cbf215 100644
--- a/nikola/plugins/task/archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,231 +24,216 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the post archives."""
+"""Classify the posts in archives."""
-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, adjust_name_for_index_path, adjust_name_for_index_link
+from collections import defaultdict
+
+import natsort
+import nikola.utils
+from nikola.plugin_categories import Taxonomy
+
+
+class Archive(Taxonomy):
+ """Classify the post archives."""
+
+ name = "classify_archive"
+
+ classification_name = "archive"
+ overview_page_variable_name = "archive"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = True
+ include_posts_into_hierarchy_root = True
+ subcategories_list_template = "list.tmpl"
+ template_for_classification_overview = None
+ always_disable_rss = True
+ always_disable_atom = True
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = False
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'archive_index': False,
+ 'archive': """Link to archive path, name is the year.
-class Archive(Task):
- """Render the post archives."""
+ Example:
- name = "render_archive"
+ link://archive/2013 => /archives/2013/index.html""",
+ 'archive_atom': False,
+ 'archive_rss': False,
+ }
def set_site(self, site):
"""Set Nikola 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):
- """Prepare an archive task."""
- # name: used to build permalink and destination
- # posts, items: posts or items; only one of them should be used,
- # the other should 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
- task_cfg = [copy.copy(kw)]
- context = {}
- context["lang"] = lang
- context["title"] = title
- context["permalink"] = self.site.link("archive", name, lang)
- context["pagekind"] = ["list", "archive_page"]
- if posts is not None:
- context["posts"] = posts
- # Depend on all post metadata because it can be used in templates (Issue #1931)
- task_cfg.append([repr(p) for p in posts])
+ # Sanity checks
+ if (site.config['CREATE_MONTHLY_ARCHIVE'] and site.config['CREATE_SINGLE_ARCHIVE']) and not site.config['CREATE_FULL_ARCHIVES']:
+ raise Exception('Cannot create monthly and single archives at the same time.')
+ # Finish setup
+ self.show_list_as_subcategories_list = not site.config['CREATE_FULL_ARCHIVES']
+ self.show_list_as_index = site.config['ARCHIVES_ARE_INDEXES']
+ self.template_for_single_list = "archiveindex.tmpl" if site.config['ARCHIVES_ARE_INDEXES'] else "archive.tmpl"
+ # Determine maximum hierarchy height
+ if site.config['CREATE_DAILY_ARCHIVE'] or site.config['CREATE_FULL_ARCHIVES']:
+ self.max_levels = 3
+ elif site.config['CREATE_MONTHLY_ARCHIVE']:
+ self.max_levels = 2
+ elif site.config['CREATE_SINGLE_ARCHIVE']:
+ self.max_levels = 0
else:
- # Depend on the content of items, to rebuild if links change (Issue #1931)
- context["items"] = items
- task_cfg.append(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 = {i: x for i, x in enumerate(task_cfg)}
- 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):
- """Genereate a task for an archive with posts."""
- 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')]
- context = {"archive_name": name,
- "is_feed_stale": kw["is_feed_stale"],
- "pagekind": ["index", "archive_page"]}
- yield self.site.generic_index_renderer(
- lang,
- posts,
- title,
- "archiveindex.tmpl",
- context,
- kw,
- str(self.name),
- page_link,
- page_path,
- uptodate)
+ self.max_levels = 1
+ return super().set_site(site)
+
+ def get_implicit_classifications(self, lang):
+ """Return a list of classification strings which should always appear in posts_per_classification."""
+ return ['']
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ levels = [str(post.date.year).zfill(4), str(post.date.month).zfill(2), str(post.date.day).zfill(2)]
+ return ['/'.join(levels[:self.max_levels])]
+
+ def sort_classifications(self, classifications, lang, level=None):
+ """Sort the given list of classification strings."""
+ if level in (0, 1):
+ # Years or months: sort descending
+ classifications.sort()
+ classifications.reverse()
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ classification = self.extract_hierarchy(classification)
+ if len(classification) == 0:
+ return self.site.MESSAGES[lang]['Archive']
+ elif len(classification) == 1:
+ return classification[0]
+ elif len(classification) == 2:
+ if only_last_component:
+ date_str = "{month}"
+ else:
+ date_str = "{month_year}"
+ return nikola.utils.LocaleBorg().format_date_in_string(
+ date_str,
+ datetime.date(int(classification[0]), int(classification[1]), 1),
+ lang)
+ else:
+ if only_last_component:
+ return str(classification[2])
+ return nikola.utils.LocaleBorg().format_date_in_string(
+ "{month_day_year}",
+ datetime.date(int(classification[0]), int(classification[1]), int(classification[2])),
+ lang)
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ components = [self.site.config['ARCHIVE_PATH'](lang)]
+ if classification:
+ components.extend(classification)
+ add_index = 'always'
else:
- yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable)
+ components.append(self.site.config['ARCHIVE_FILENAME'](lang))
+ add_index = 'never'
+ return [_f for _f in components if _f], add_index
+
+ def extract_hierarchy(self, classification):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return classification.split('/') if classification else []
- def gen_tasks(self):
- """Generate archive tasks."""
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return '/'.join(hierarchy)
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ hierarchy = self.extract_hierarchy(classification)
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']) and not kw['create_full_archives']:
- raise Exception('Cannot create monthly and single archives at the same time.')
- for lang in kw["translations"]:
- 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():
- # 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:
- title = kw["messages"][lang]["Posts for year %s"] % year
- kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != year)
- else:
- 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), len(self.site.posts_per_month[m])) 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, count] for month, link, count in months]
- yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable)
-
- 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
- for yearmonth, posts in self.site.posts_per_month.items():
- # Add archive per month
- year, month = yearmonth.split('/')
-
- 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)
- kw['years'] = years
- for lang in kw["translations"]:
- items = [(y, self.site.link("archive", y, lang), len(self.site.posts_per_year[y])) 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):
- """Link to archive path, name is the year.
-
- Example:
-
- link://archive/2013 => /archives/2013/index.html
- """
- 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,
- index_file] if _f]
+ page_kind = "list"
+ if self.show_list_as_index:
+ if not self.show_list_as_subcategories_list or len(hierarchy) == self.max_levels:
+ page_kind = "index"
+ if len(hierarchy) == 0:
+ title = kw["messages"][lang]["Archive"]
+ elif len(hierarchy) == 1:
+ title = kw["messages"][lang]["Posts for year %s"] % hierarchy[0]
+ elif len(hierarchy) == 2:
+ title = nikola.utils.LocaleBorg().format_date_in_string(
+ kw["messages"][lang]["Posts for {month_year}"],
+ datetime.date(int(hierarchy[0]), int(hierarchy[1]), 1),
+ lang)
+ elif len(hierarchy) == 3:
+ title = nikola.utils.LocaleBorg().format_date_in_string(
+ kw["messages"][lang]["Posts for {month_day_year}"],
+ datetime.date(int(hierarchy[0]), int(hierarchy[1]), int(hierarchy[2])),
+ lang)
else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['ARCHIVE_PATH'],
- archive_file] if _f]
+ raise Exception("Cannot interpret classification {}!".format(repr(classification)))
- def archive_atom_path(self, name, lang):
- """Link to atom archive path, name is the year.
-
- Example:
+ context = {
+ "title": title,
+ "pagekind": [page_kind, "archive_page"],
+ "create_archive_navigation": self.site.config["CREATE_ARCHIVE_NAVIGATION"],
+ "archive_name": classification
+ }
- link://archive_atom/2013 => /archives/2013/index.atom
- """
- return self.archive_path(name, lang, is_feed=True)
+ # Generate links for hierarchies
+ if context["create_archive_navigation"]:
+ if hierarchy:
+ # Up level link makes sense only if this is not the top-level
+ # page (hierarchy is empty)
+ parent = '/'.join(hierarchy[:-1])
+ context["up_archive"] = self.site.link('archive', parent, lang)
+ context["up_archive_name"] = self.get_classification_friendly_name(parent, lang)
+ else:
+ context["up_archive"] = None
+ context["up_archive_name"] = None
+
+ nodelevel = len(hierarchy)
+ flat_samelevel = self.archive_navigation[lang][nodelevel]
+ idx = flat_samelevel.index(classification)
+ if idx == -1:
+ raise Exception("Cannot find classification {0} in flat hierarchy!".format(classification))
+ previdx, nextidx = idx - 1, idx + 1
+ # If the previous index is -1, or the next index is 1, the previous/next archive does not exist.
+ context["previous_archive"] = self.site.link('archive', flat_samelevel[previdx], lang) if previdx != -1 else None
+ context["previous_archive_name"] = self.get_classification_friendly_name(flat_samelevel[previdx], lang) if previdx != -1 else None
+ context["next_archive"] = self.site.link('archive', flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None
+ context["next_archive_name"] = self.get_classification_friendly_name(flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None
+ context["archive_nodelevel"] = nodelevel
+ context["has_archive_navigation"] = bool(context["previous_archive"] or context["up_archive"] or context["next_archive"])
+ else:
+ context["has_archive_navigation"] = False
+ kw.update(context)
+ return context, kw
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ # Build a lookup table for archive navigation, if we’ll need one.
+ if self.site.config['CREATE_ARCHIVE_NAVIGATION']:
+ if flat_hierarchy_per_lang is None:
+ raise ValueError('Archives need flat_hierarchy_per_lang')
+ self.archive_navigation = {}
+ for lang, flat_hierarchy in flat_hierarchy_per_lang.items():
+ self.archive_navigation[lang] = defaultdict(list)
+ for node in flat_hierarchy:
+ if not self.site.config["SHOW_UNTRANSLATED_POSTS"]:
+ if not [x for x in posts_per_classification_per_language[lang][node.classification_name] if x.is_translation_available(lang)]:
+ continue
+ self.archive_navigation[lang][len(node.classification_path)].append(node.classification_name)
+
+ # We need to sort it. Natsort means it’s year 10000 compatible!
+ for k, v in self.archive_navigation[lang].items():
+ self.archive_navigation[lang][k] = natsort.natsorted(v, alg=natsort.ns.F | natsort.ns.IC)
+
+ return super().postprocess_posts_per_classification(posts_per_classification_per_language, flat_hierarchy_per_lang, hierarchy_lookup_per_lang)
+
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ return classification == '' or len(post_list) > 0
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same classification in other languages."""
+ return [(other_lang, classification) for other_lang, lookup in classifications_per_language.items() if classification in lookup and other_lang != lang]
diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin
index 3fc4ef2..19e687c 100644
--- a/nikola/plugins/task/authors.plugin
+++ b/nikola/plugins/task/authors.plugin
@@ -1,5 +1,5 @@
[Core]
-Name = render_authors
+Name = classify_authors
Module = authors
[Documentation]
@@ -8,3 +8,5 @@ Version = 0.1
Website = http://getnikola.com
Description = Render the author pages and feeds.
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py
index ec61800..24fe650 100644
--- a/nikola/plugins/task/authors.py
+++ b/nikola/plugins/task/authors.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2015-2016 Juanjo Conti and others.
+# Copyright © 2015-2020 Juanjo Conti and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,301 +26,134 @@
"""Render the author pages and feeds."""
-from __future__ import unicode_literals
-import os
-import natsort
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from collections import defaultdict
-from blinker import signal
-
-from nikola.plugin_categories import Task
+from nikola.plugin_categories import Taxonomy
from nikola import utils
-class RenderAuthors(Task):
- """Render the author pages and feeds."""
-
- name = "render_authors"
- posts_per_author = None
-
- def set_site(self, site):
- """Set Nikola site."""
- self.generate_author_pages = False
- if site.config["ENABLE_AUTHOR_PAGES"]:
- site.register_path_handler('author_index', self.author_index_path)
- site.register_path_handler('author', self.author_path)
- site.register_path_handler('author_atom', self.author_atom_path)
- site.register_path_handler('author_rss', self.author_rss_path)
- signal('scanned').connect(self.posts_scanned)
- return super(RenderAuthors, self).set_site(site)
-
- def posts_scanned(self, event):
- """Called after posts are scanned via signal."""
- self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and len(self._posts_per_author()) > 1
- self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages
-
- def gen_tasks(self):
- """Render the author pages and feeds."""
- kw = {
- "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'],
- 'author_path': self.site.config['AUTHOR_PATH'],
- "author_pages_are_indexes": self.site.config['AUTHOR_PAGES_ARE_INDEXES'],
- "generate_rss": self.site.config['GENERATE_RSS'],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "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()
- yield self.group_task()
-
- if self.generate_author_pages:
- yield self.list_authors_page(kw)
-
- if not self._posts_per_author(): # this may be self.site.posts_per_author
- return
-
- author_list = list(self._posts_per_author().items())
+class ClassifyAuthors(Taxonomy):
+ """Classify the posts by authors."""
- def render_lists(author, posts):
- """Render author pages as RSS files and lists/indexes."""
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for lang in kw["translations"]:
- if kw["show_untranslated_posts"]:
- filtered_posts = post_list
- else:
- filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
- if kw["generate_rss"]:
- yield self.author_rss(author, lang, filtered_posts, kw)
- # Render HTML
- if kw['author_pages_are_indexes']:
- yield self.author_page_as_index(author, lang, filtered_posts, kw)
- else:
- yield self.author_page_as_list(author, lang, filtered_posts, kw)
+ name = "classify_authors"
- for author, posts in author_list:
- for task in render_lists(author, posts):
- yield task
+ classification_name = "author"
+ overview_page_variable_name = "authors"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = False
+ template_for_classification_overview = "authors.tmpl"
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = False
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'author_index': """ Link to the authors index.
- def _create_authors_page(self, kw):
- """Create a global "all authors" page for each language."""
- template_name = "authors.tmpl"
- kw = kw.copy()
- for lang in kw["translations"]:
- authors = natsort.natsorted([author for author in self._posts_per_author().keys()],
- alg=natsort.ns.F | natsort.ns.IC)
- has_authors = (authors != [])
- kw['authors'] = authors
- output_name = os.path.join(
- kw['output_folder'], self.site.path('author_index', None, lang))
- context = {}
- if has_authors:
- context["title"] = kw["messages"][lang]["Authors"]
- context["items"] = [(author, self.site.link("author", author, lang)) for author
- in authors]
- context["description"] = context["title"]
- else:
- context["items"] = None
- context["permalink"] = self.site.link("author_index", None, lang)
- context["pagekind"] = ["list", "authors_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:page')]
- task['basename'] = str(self.name)
- yield task
+ Example:
- def list_authors_page(self, kw):
- """Create a global "all authors" page for each language."""
- yield self._create_authors_page(kw)
+ link://authors/ => /authors/index.html""",
+ 'author': """Link to an author's page.
- def _get_title(self, author):
- return author
+ Example:
- def _get_description(self, author, lang):
- descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS']
- return descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None
+ link://author/joe => /authors/joe.html""",
+ 'author_atom': """Link to an author's Atom feed.
- def author_page_as_index(self, author, lang, post_list, kw):
- """Render a sort of index page collection using only this author's posts."""
- kind = "author"
+Example:
- 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, author, lang), i, displayed_i, lang, self.site, force_addition, extension)
+link://author_atom/joe => /authors/joe.atom""",
+ 'author_rss': """Link to an author's RSS feed.
- 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, author, lang), i, displayed_i, lang, self.site, force_addition, extension)
+Example:
- context_source = {}
- title = self._get_title(author)
- if kw["generate_rss"]:
- # On a author page, the feeds include the author's feeds
- rss_link = ("""<link rel="alternate" type="application/rss+xml" """
- """title="RSS for author """
- """{0} ({1})" href="{2}">""".format(
- title, lang, self.site.link(kind + "_rss", author, lang)))
- context_source['rss_link'] = rss_link
- context_source["author"] = title
- indexes_title = kw["messages"][lang]["Posts by %s"] % title
- context_source["description"] = self._get_description(author, lang)
- context_source["pagekind"] = ["index", "author_page"]
- template_name = "authorindex.tmpl"
+link://author_rss/joe => /authors/joe.xml""",
+ }
- yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path)
+ def set_site(self, site):
+ """Set Nikola site."""
+ super().set_site(site)
+ self.show_list_as_index = site.config['AUTHOR_PAGES_ARE_INDEXES']
+ self.more_than_one_classifications_per_post = site.config.get('MULTIPLE_AUTHORS_PER_POST', False)
+ self.template_for_single_list = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl"
+ self.translation_manager = utils.ClassificationTranslationManager()
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ if not self.site.config["ENABLE_AUTHOR_PAGES"]:
+ return False
+ if lang is not None:
+ return self.generate_author_pages
+ return True
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ if self.more_than_one_classifications_per_post:
+ return post.authors(lang=lang)
+ else:
+ return [post.author(lang=lang)]
- def author_page_as_list(self, author, lang, post_list, kw):
- """Render a single flat link list with this author's posts."""
- kind = "author"
- template_name = "author.tmpl"
- output_name = os.path.join(kw['output_folder'], self.site.path(
- kind, author, lang))
- context = {}
- context["lang"] = lang
- title = self._get_title(author)
- context["author"] = title
- context["title"] = kw["messages"][lang]["Posts by %s"] % title
- context["posts"] = post_list
- context["permalink"] = self.site.link(kind, author, lang)
- context["kind"] = kind
- context["description"] = self._get_description(author, lang)
- context["pagekind"] = ["list", "author_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:list')]
- task['basename'] = str(self.name)
- yield task
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return classification
- def author_rss(self, author, lang, posts, kw):
- """Create a RSS feed for a single author in a given language."""
- kind = "author"
- # Render RSS
- output_name = os.path.normpath(
- os.path.join(kw['output_folder'],
- self.site.path(kind + "_rss", author, lang)))
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", author, 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)
- 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), self._get_title(author)),
- kw["site_url"], None, post_list,
- output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
- feed_url, None, kw["feed_link_append_query"]))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.authors:rss')] + deps_uptodate,
- 'task_dep': ['render_posts'],
- }
- return utils.apply_filters(task, kw['filters'])
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ path = self.site.config['AUTHOR_PATH'](lang)
+ return [component for component in path.split('/') if component], 'always'
- def slugify_author_name(self, name, lang=None):
- """Slugify an author name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderAuthors.slugify_author_name() called without language!")
- lang = ''
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
if self.site.config['SLUG_AUTHOR_PATH']:
- name = utils.slugify(name, lang)
- return name
-
- def author_index_path(self, name, lang):
- """Link to the author's index.
-
- Example:
-
- link://authors/ => /authors/index.html
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.site.config['INDEX_FILE']] if _f]
-
- def author_path(self, name, lang):
- """Link to an author's page.
-
- Example:
-
- link://author/joe => /authors/joe.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.slugify_author_name(name, lang),
- self.site.config['INDEX_FILE']] if _f]
+ slug = utils.slugify(classification, lang)
else:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.slugify_author_name(name, lang) + ".html"] if _f]
-
- def author_atom_path(self, name, lang):
- """Link to an author's Atom feed.
-
- Example:
-
- link://author_atom/joe => /authors/joe.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".atom"] if
- _f]
-
- def author_rss_path(self, name, lang):
- """Link to an author's RSS feed.
-
- Example:
-
- link://author_rss/joe => /authors/joe.rss
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".xml"] if
- _f]
+ slug = classification
+ return [self.site.config['AUTHOR_PATH'](lang), slug], 'auto'
- def _add_extension(self, path, extension):
- path[-1] += extension
- return path
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ "messages": self.site.MESSAGES,
+ }
+ context = {
+ "title": kw["messages"][lang]["Authors"],
+ "description": kw["messages"][lang]["Authors"],
+ "permalink": self.site.link("author_index", None, lang),
+ "pagekind": ["list", "authors_page"],
+ }
+ kw.update(context)
+ return context, kw
- def _posts_per_author(self):
- """Return a dict of posts per author."""
- if self.posts_per_author is None:
- self.posts_per_author = defaultdict(list)
- for post in self.site.timeline:
- if post.is_post:
- self.posts_per_author[post.author()].append(post)
- return self.posts_per_author
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS']
+ kw = {
+ "messages": self.site.MESSAGES,
+ }
+ context = {
+ "author": classification,
+ "title": kw["messages"][lang]["Posts by %s"] % classification,
+ "description": descriptions[lang][classification] if lang in descriptions and classification in descriptions[lang] else None,
+ "pagekind": ["index" if self.show_list_as_index else "list", "author_page"],
+ }
+ kw.update(context)
+ return context, kw
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same author in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ more_than_one = False
+ for lang, posts_per_author in posts_per_classification_per_language.items():
+ authors = set()
+ for author, posts in posts_per_author.items():
+ for post in posts:
+ if not self.site.config["SHOW_UNTRANSLATED_POSTS"] and not post.is_translation_available(lang):
+ continue
+ authors.add(author)
+ if len(authors) > 1:
+ more_than_one = True
+ self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one
+ self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages
+ self.translation_manager.add_defaults(posts_per_classification_per_language)
diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin
index b5bf6e4..939065b 100644
--- a/nikola/plugins/task/bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -6,8 +6,8 @@ module = bundles
author = Roberto Alsina
version = 1.0
website = https://getnikola.com/
-description = Theme bundles using WebAssets
+description = Bundle assets
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py
index b33d8e0..aa4ce78 100644
--- a/nikola/plugins/task/bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,38 +24,26 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Bundle assets using WebAssets."""
+"""Bundle assets."""
-from __future__ import unicode_literals
+import configparser
+import io
+import itertools
import os
-
-try:
- import webassets
-except ImportError:
- webassets = None # NOQA
+import shutil
from nikola.plugin_categories import LateTask
from nikola import utils
class BuildBundles(LateTask):
- """Bundle assets using WebAssets."""
+ """Bundle assets."""
name = "create_bundles"
- def set_site(self, site):
- """Set Nikola site."""
- self.logger = utils.get_logger('bundles', utils.STDERR_HANDLER)
- if webassets is None and site.config['USE_BUNDLES']:
- utils.req_missing(['webassets'], 'USE_BUNDLES', optional=True)
- self.logger.warn('Setting USE_BUNDLES to False.')
- site.config['USE_BUNDLES'] = False
- site._GLOBAL_CONTEXT['use_bundles'] = False
- super(BuildBundles, self).set_site(site)
-
def gen_tasks(self):
- """Bundle assets using WebAssets."""
+ """Bundle assets."""
kw = {
'filters': self.site.config['FILTERS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
@@ -69,28 +57,21 @@ class BuildBundles(LateTask):
def build_bundle(output, inputs):
out_dir = os.path.join(kw['output_folder'],
os.path.dirname(output))
- inputs = [os.path.relpath(i, out_dir) for i in inputs if os.path.isfile(i)]
- cache_dir = os.path.join(kw['cache_folder'], 'webassets')
- utils.makedirs(cache_dir)
- env = webassets.Environment(out_dir, os.path.dirname(output),
- cache=cache_dir)
- if inputs:
- bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
- env.register(output, bundle)
- # This generates the file
- 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
+ inputs = [
+ os.path.join(
+ out_dir,
+ os.path.relpath(i, out_dir))
+ for i in inputs if os.path.isfile(i)
+ ]
+ with open(os.path.join(out_dir, os.path.basename(output)), 'wb+') as out_fh:
+ for i in inputs:
+ with open(i, 'rb') as in_fh:
+ shutil.copyfileobj(in_fh, out_fh)
+ out_fh.write(b'\n')
yield self.group_task()
- if (webassets is not None and self.site.config['USE_BUNDLES'] is not
- False):
+
+ if self.site.config['USE_BUNDLES']:
for name, _files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
@@ -127,19 +108,17 @@ class BuildBundles(LateTask):
def get_theme_bundles(themes):
"""Given a theme chain, return the bundle definitions."""
- bundles = {}
for theme_name in themes:
bundles_path = os.path.join(
utils.get_theme_path(theme_name), 'bundles')
if os.path.isfile(bundles_path):
- with open(bundles_path) as fd:
- for line in fd:
- try:
- name, files = line.split('=')
- files = [f.strip() for f in files.split(',')]
- bundles[name.strip().replace('/', os.sep)] = files
- except ValueError:
- # for empty lines
- pass
- break
- return bundles
+ config = configparser.ConfigParser()
+ header = io.StringIO('[bundles]\n')
+ with open(bundles_path, 'rt') as fd:
+ config.read_file(itertools.chain(header, fd))
+ bundles = {}
+ for name, files in config['bundles'].items():
+ name = name.strip().replace('/', os.sep)
+ files = [f.strip() for f in files.split(',') if f.strip()]
+ bundles[name] = files
+ return bundles
diff --git a/nikola/plugins/task/categories.plugin b/nikola/plugins/task/categories.plugin
new file mode 100644
index 0000000..be2bb79
--- /dev/null
+++ b/nikola/plugins/task/categories.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = classify_categories
+module = categories
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Render the category pages and feeds.
+
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/categories.py b/nikola/plugins/task/categories.py
new file mode 100644
index 0000000..68f9caa
--- /dev/null
+++ b/nikola/plugins/task/categories.py
@@ -0,0 +1,248 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the category pages and feeds."""
+
+import os
+
+from nikola.plugin_categories import Taxonomy
+from nikola import utils, hierarchy_utils
+
+
+class ClassifyCategories(Taxonomy):
+ """Classify the posts by categories."""
+
+ name = "classify_categories"
+
+ classification_name = "category"
+ overview_page_variable_name = "categories"
+ overview_page_items_variable_name = "cat_items"
+ overview_page_hierarchy_variable_name = "cat_hierarchy"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = True
+ include_posts_into_hierarchy_root = False
+ show_list_as_subcategories_list = False
+ template_for_classification_overview = "tags.tmpl"
+ always_disable_rss = False
+ always_disable_atom = False
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = True
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'category_index': """A link to the category index.
+
+Example:
+
+link://category_index => /categories/index.html""",
+ 'category': """A link to a category. Takes page number as optional keyword argument.
+
+Example:
+
+link://category/dogs => /categories/dogs.html""",
+ 'category_atom': """A link to a category's Atom feed.
+
+Example:
+
+link://category_atom/dogs => /categories/dogs.atom""",
+ 'category_rss': """A link to a category's RSS feed.
+
+Example:
+
+link://category_rss/dogs => /categories/dogs.xml""",
+ }
+
+ def set_site(self, site):
+ """Set site, which is a Nikola instance."""
+ super().set_site(site)
+ self.show_list_as_index = self.site.config['CATEGORY_PAGES_ARE_INDEXES']
+ self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl"
+ self.translation_manager = utils.ClassificationTranslationManager()
+
+ # Needed to undo names for CATEGORY_PAGES_FOLLOW_DESTPATH
+ self.destpath_names_reverse = {}
+ for lang in self.site.config['TRANSLATIONS']:
+ self.destpath_names_reverse[lang] = {}
+ for k, v in self.site.config['CATEGORY_DESTPATH_NAMES'](lang).items():
+ self.destpath_names_reverse[lang][v] = k
+ self.destpath_names_reverse = utils.TranslatableSetting(
+ '_CATEGORY_DESTPATH_NAMES_REVERSE', self.destpath_names_reverse,
+ self.site.config['TRANSLATIONS'])
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return True
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ cat = post.meta('category', lang=lang).strip()
+ return [cat] if cat else []
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ classification = self.extract_hierarchy(classification)
+ return classification[-1] if classification else ''
+
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ if self.site.config['CATEGORIES_INDEX_PATH'](lang):
+ path = self.site.config['CATEGORIES_INDEX_PATH'](lang)
+ append_index = 'never'
+ else:
+ path = self.site.config['CATEGORY_PATH'](lang)
+ append_index = 'always'
+ return [component for component in path.split('/') if component], append_index
+
+ def slugify_tag_name(self, name, lang):
+ """Slugify a tag name."""
+ if self.site.config['SLUG_TAG_PATH']:
+ name = utils.slugify(name, lang)
+ return name
+
+ def slugify_category_name(self, path, lang):
+ """Slugify a category name."""
+ if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']:
+ path = path[-1:] # only the leaf
+ result = [self.slugify_tag_name(part, lang) 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 get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ cat_string = '/'.join(classification)
+ classification_raw = classification # needed to undo CATEGORY_DESTPATH_NAMES
+ destpath_names_reverse = self.destpath_names_reverse(lang)
+ if self.site.config['CATEGORY_PAGES_FOLLOW_DESTPATH']:
+ base_dir = None
+ for post in self.site.posts_per_category[cat_string]:
+ if post.category_from_destpath:
+ base_dir = post.folder_base(lang)
+ # Handle CATEGORY_DESTPATH_NAMES
+ if cat_string in destpath_names_reverse:
+ cat_string = destpath_names_reverse[cat_string]
+ classification_raw = cat_string.split('/')
+ break
+
+ if not self.site.config['CATEGORY_DESTPATH_TRIM_PREFIX']:
+ # If prefixes are not trimmed, we'll already have the base_dir in classification_raw
+ base_dir = ''
+
+ if base_dir is None:
+ # fallback: first POSTS entry + classification
+ base_dir = self.site.config['POSTS'][0][1]
+ base_dir_list = base_dir.split(os.sep)
+ sub_dir = [self.slugify_tag_name(part, lang) for part in classification_raw]
+ return [_f for _f in (base_dir_list + sub_dir) if _f], 'auto'
+ else:
+ return [_f for _f in [self.site.config['CATEGORY_PATH'](lang)] if _f] + self.slugify_category_name(
+ classification, lang), 'auto'
+
+ def extract_hierarchy(self, classification):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return hierarchy_utils.parse_escaped_hierarchical_category_name(classification)
+
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return hierarchy_utils.join_hierarchical_category_path(hierarchy)
+
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ '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'],
+ "tzinfo": self.site.tzinfo,
+ "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
+ "category_titles": self.site.config['CATEGORY_TITLES'],
+ }
+ context = {
+ "title": self.site.MESSAGES[lang]["Categories"],
+ "description": self.site.MESSAGES[lang]["Categories"],
+ "pagekind": ["list", "tags_page"],
+ }
+ kw.update(context)
+ return context, kw
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ cat_path = self.extract_hierarchy(classification)
+ kw = {
+ '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'],
+ "tzinfo": self.site.tzinfo,
+ "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
+ "category_titles": self.site.config['CATEGORY_TITLES'],
+ }
+ posts = self.site.posts_per_classification[self.classification_name][lang]
+ if node is None:
+ children = []
+ else:
+ children = [child for child in node.children if len([post for post in posts.get(child.classification_name, []) if self.site.config['SHOW_UNTRANSLATED_POSTS'] or post.is_translation_available(lang)]) > 0]
+ subcats = [(child.name, self.site.link(self.classification_name, child.classification_name, lang)) for child in children]
+ friendly_name = self.get_classification_friendly_name(classification, lang)
+ context = {
+ "title": self.site.config['CATEGORY_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % friendly_name),
+ "description": self.site.config['CATEGORY_DESCRIPTIONS'].get(lang, {}).get(classification),
+ "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"],
+ "tag": friendly_name,
+ "category": classification,
+ "category_path": cat_path,
+ "subcategories": subcats,
+ }
+ kw.update(context)
+ return context, kw
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same category in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ self.translation_manager.read_from_config(self.site, 'CATEGORY', posts_per_classification_per_language, False)
+
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ if self.site.config["CATEGORY_PAGES_FOLLOW_DESTPATH"]:
+ # In destpath mode, allow users to replace the default category index with a custom page.
+ classification_hierarchy = self.extract_hierarchy(classification)
+ dest_list, _ = self.get_path(classification_hierarchy, lang)
+ short_destination = os.sep.join(dest_list + [self.site.config["INDEX_FILE"]])
+ if short_destination in self.site.post_per_file:
+ return False
+ return True
+
+ def should_generate_atom_for_classification_page(self, classification, post_list, lang):
+ """Only generates Atom feed for list of posts for classification if this function returns True."""
+ return True
+
+ def should_generate_rss_for_classification_page(self, classification, post_list, lang):
+ """Only generates RSS feed for list of posts for classification if this function returns True."""
+ return True
diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index ddd38df..b63581d 100644
--- a/nikola/plugins/task/copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy theme assets into output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py
index 4ed7414..c6d32c7 100644
--- a/nikola/plugins/task/copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,11 +26,11 @@
"""Copy theme assets into output."""
-from __future__ import unicode_literals
import io
import os
+from nikola.packages.pygments_better_html import BetterHtmlFormatter
from nikola.plugin_categories import Task
from nikola import utils
@@ -48,13 +48,19 @@ class CopyAssets(Task):
"""
kw = {
"themes": self.site.THEMES,
+ "translations": self.site.translations,
"files_folders": self.site.config['FILES_FOLDERS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
- "code.css_selectors": 'pre.code',
+ "code.css_selectors": ['pre.code', '.code .codetable', '.highlight pre'],
+ "code.css_wrappers": ['.highlight', '.code'],
"code.css_head": '/* code.css file generated by Nikola */\n',
- "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n",
+ "code.css_close": (
+ "\ntable.codetable, table.highlighttable { width: 100%;}\n"
+ ".codetable td.linenos, td.linenos { text-align: right; width: 3.5em; "
+ "padding-right: 0.5em; background: rgba(127, 127, 127, 0.2) }\n"
+ ".codetable td.code, td.code { padding-left: 0.5em; }\n"),
}
tasks = {}
code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
@@ -63,11 +69,20 @@ class CopyAssets(Task):
files_folders=kw['files_folders'], output_dir=None)
yield self.group_task()
+ main_theme = utils.get_theme_path(kw['themes'][0])
+ theme_ini = utils.parse_theme_meta(main_theme)
+ if theme_ini:
+ ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',')
+ ignored_assets = [os.path.normpath(asset_name.strip()) for asset_name in ignored_assets]
+ else:
+ ignored_assets = []
+
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
- if task['name'] in tasks:
+ asset_name = os.path.relpath(task['name'], dst)
+ if task['name'] in tasks or asset_name in ignored_assets:
continue
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')]
@@ -79,18 +94,18 @@ class CopyAssets(Task):
yield utils.apply_filters(task, kw['filters'])
# Check whether or not there is a code.css file around.
- if not code_css_input:
+ if not code_css_input and kw['code_color_scheme']:
def create_code_css():
- from pygments.formatters import get_formatter_by_name
- formatter = get_formatter_by_name('html', style=kw["code_color_scheme"])
+ formatter = BetterHtmlFormatter(style=kw["code_color_scheme"])
utils.makedirs(os.path.dirname(code_css_path))
- with io.open(code_css_path, 'w+', encoding='utf8') as outf:
+ with io.open(code_css_path, 'w+', encoding='utf-8') as outf:
outf.write(kw["code.css_head"])
- outf.write(formatter.get_style_defs(kw["code.css_selectors"]))
+ outf.write(formatter.get_style_defs(
+ kw["code.css_selectors"], kw["code.css_wrappers"]))
outf.write(kw["code.css_close"])
if os.path.exists(code_css_path):
- with io.open(code_css_path, 'r', encoding='utf-8') as fh:
+ with io.open(code_css_path, 'r', encoding='utf-8-sig') as fh:
testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"]
else:
testcontents = False
diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index e4bb1cf..45c2253 100644
--- a/nikola/plugins/task/copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy static files into the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py
index 6f6cfb8..26364d4 100644
--- a/nikola/plugins/task/copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 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/galleries.plugin b/nikola/plugins/task/galleries.plugin
index 2064e68..d06e117 100644
--- a/nikola/plugins/task/galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create image galleries automatically.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
index edfd33d..b8ac9ee 100644
--- a/nikola/plugins/task/galleries.py
+++ b/nikola/plugins/task/galleries.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,32 +26,29 @@
"""Render image galleries."""
-from __future__ import unicode_literals
import datetime
import glob
import io
import json
import mimetypes
import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
+from collections import OrderedDict
+from urllib.parse import urljoin
import natsort
-try:
- from PIL import Image # NOQA
-except ImportError:
- import Image as _Image
- Image = _Image
-
import PyRSS2Gen as rss
+from PIL import Image
from nikola.plugin_categories import Task
from nikola import utils
from nikola.image_processing import ImageProcessor
from nikola.post import Post
+try:
+ from ruamel.yaml import YAML
+except ImportError:
+ YAML = None
+
_image_size_cache = {}
@@ -63,12 +60,11 @@ class Galleries(Task, ImageProcessor):
def set_site(self, site):
"""Set Nikola site."""
+ super().set_site(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', utils.STDERR_HANDLER)
-
self.kw = {
'thumbnail_size': site.config['THUMBNAIL_SIZE'],
'max_image_size': site.config['MAX_IMAGE_SIZE'],
@@ -87,6 +83,11 @@ class Galleries(Task, ImageProcessor):
'generate_rss': site.config['GENERATE_RSS'],
'preserve_exif_data': site.config['PRESERVE_EXIF_DATA'],
'exif_whitelist': site.config['EXIF_WHITELIST'],
+ 'preserve_icc_profiles': site.config['PRESERVE_ICC_PROFILES'],
+ 'index_path': site.config['INDEX_PATH'],
+ 'disable_indexes': site.config['DISABLE_INDEXES'],
+ 'galleries_use_thumbnail': site.config['GALLERIES_USE_THUMBNAIL'],
+ 'galleries_default_thumbnail': site.config['GALLERIES_DEFAULT_THUMBNAIL'],
}
# Verify that no folder in GALLERY_FOLDERS appears twice
@@ -104,8 +105,6 @@ class Galleries(Task, ImageProcessor):
# 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.
@@ -165,7 +164,7 @@ class Galleries(Task, ImageProcessor):
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]
+ [self.site.config['RSS_FILENAME_BASE'](lang) + self.site.config['RSS_EXTENSION']] if _f]
def gen_tasks(self):
"""Render image galleries."""
@@ -173,7 +172,7 @@ class Galleries(Task, ImageProcessor):
self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- self.kw['||template_hooks|{0}||'.format(k)] = v._items
+ self.kw['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
self.site.scan_posts()
yield self.group_task()
@@ -223,6 +222,12 @@ class Galleries(Task, ImageProcessor):
self.kw[k] = self.site.GLOBAL_CONTEXT[k](lang)
context = {}
+
+ # Do we have a metadata file?
+ meta_path, order, captions, img_metadata = self.find_metadata(gallery, lang)
+ context['meta_path'] = meta_path
+ context['order'] = order
+ context['captions'] = captions
context["lang"] = lang
if post:
context["title"] = post.title(lang)
@@ -232,7 +237,20 @@ class Galleries(Task, ImageProcessor):
image_name_list = [os.path.basename(p) for p in image_list]
- if self.kw['use_filename_as_title']:
+ if captions:
+ img_titles = []
+ for fn in image_name_list:
+ if fn in captions:
+ img_titles.append(captions[fn])
+ else:
+ if self.kw['use_filename_as_title']:
+ img_titles.append(fn)
+ else:
+ img_titles.append('')
+ self.logger.debug(
+ "Image {0} found in gallery but not listed in {1}".
+ format(fn, context['meta_path']))
+ elif self.kw['use_filename_as_title']:
img_titles = []
for fn in image_name_list:
name_without_ext = os.path.splitext(os.path.basename(fn))[0]
@@ -248,6 +266,7 @@ class Galleries(Task, ImageProcessor):
folders = []
# Generate friendly gallery names
+ fpost_list = []
for path, folder in folder_list:
fpost = self.parse_index(path, input_folder, output_folder)
if fpost:
@@ -256,8 +275,17 @@ class Galleries(Task, ImageProcessor):
ft = folder
if not folder.endswith('/'):
folder += '/'
- folders.append((folder, ft))
+ # TODO: This is to keep compatibility with user's custom gallery.tmpl
+ # To be removed in v9 someday
+ if self.kw['galleries_use_thumbnail']:
+ folders.append((folder, ft, fpost))
+ if fpost:
+ fpost_list.append(fpost.source_path)
+ else:
+ folders.append((folder, ft))
+
+ context["gallery_path"] = gallery
context["folders"] = natsort.natsorted(
folders, alg=natsort.ns.F | natsort.ns.IC)
context["crumbs"] = utils.get_crumbs(gallery, index_folder=self, lang=lang)
@@ -265,6 +293,7 @@ class Galleries(Task, ImageProcessor):
context["enable_comments"] = self.kw['comments_in_galleries']
context["thumbnail_size"] = self.kw["thumbnail_size"]
context["pagekind"] = ["gallery_front"]
+ context["galleries_use_thumbnail"] = self.kw['galleries_use_thumbnail']
if post:
yield {
@@ -291,7 +320,7 @@ class Galleries(Task, ImageProcessor):
yield utils.apply_filters({
'basename': self.name,
'name': dst,
- 'file_dep': file_dep,
+ 'file_dep': file_dep + dest_img_list + fpost_list,
'targets': [dst],
'actions': [
(self.render_gallery_index, (
@@ -301,7 +330,7 @@ class Galleries(Task, ImageProcessor):
dest_img_list,
img_titles,
thumbs,
- file_dep))],
+ img_metadata))],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw.copy(),
@@ -343,7 +372,14 @@ class Galleries(Task, ImageProcessor):
self.gallery_list = []
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))
+ # If output folder is empty, the top-level gallery
+ # index will collide with the main page for the site.
+ # Don't generate the top-level gallery index in that
+ # case.
+ # FIXME: also ignore pages named index
+ if (output_folder or root != input_folder and
+ (not self.kw['disable_indexes'] and self.kw['index_path'] == '')):
+ self.gallery_list.append((root, input_folder, output_folder))
def create_galleries_paths(self):
"""Given a list of galleries, put their paths into self.gallery_links."""
@@ -395,12 +431,73 @@ class Galleries(Task, ImageProcessor):
'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:mkdir')],
}
+ def find_metadata(self, gallery, lang):
+ """Search for a gallery metadata file.
+
+ If there is an metadata file for the gallery, use that to determine
+ captions and the order in which images shall be displayed in the
+ gallery. You only need to list the images if a specific ordering or
+ caption is required. The metadata file is YAML-formatted, with field
+ names of
+ #
+ name:
+ caption:
+ order:
+ #
+ If a numeric order value is specified, we use that directly, otherwise
+ we depend on how the library returns the information - which may or may not
+ be in the same order as in the file itself. Non-numeric ordering is not
+ supported. If no caption is specified, then we return an empty string.
+ Returns a string (l18n'd filename), list (ordering), dict (captions),
+ dict (image metadata).
+ """
+ base_meta_path = os.path.join(gallery, "metadata.yml")
+ localized_meta_path = utils.get_translation_candidate(self.site.config,
+ base_meta_path, lang)
+ order = []
+ captions = {}
+ custom_metadata = {}
+ used_path = ""
+
+ if os.path.isfile(localized_meta_path):
+ used_path = localized_meta_path
+ elif os.path.isfile(base_meta_path):
+ used_path = base_meta_path
+ else:
+ return "", [], {}, {}
+
+ self.logger.debug("Using {0} for gallery {1}".format(
+ used_path, gallery))
+ with open(used_path, "r", encoding='utf-8-sig') as meta_file:
+ if YAML is None:
+ utils.req_missing(['ruamel.yaml'], 'use metadata.yml files for galleries')
+ yaml = YAML(typ='safe')
+ meta = yaml.load_all(meta_file)
+ for img in meta:
+ # load_all and safe_load_all both return None as their
+ # final element, so skip it
+ if not img:
+ continue
+ if 'name' in img:
+ img_name = img.pop('name')
+ if 'caption' in img and img['caption']:
+ captions[img_name] = img.pop('caption')
+
+ if 'order' in img and img['order'] is not None:
+ order.insert(img.pop('order'), img_name)
+ else:
+ order.append(img_name)
+ custom_metadata[img_name] = img
+ else:
+ self.logger.error("no 'name:' for ({0}) in {1}".format(
+ img, used_path))
+ return used_path, order, captions, custom_metadata
+
def parse_index(self, gallery, input_folder, output_folder):
"""Return 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"], output_folder,
- os.path.relpath(gallery, input_folder))
+ destination = os.path.join(output_folder,
+ os.path.relpath(gallery, input_folder))
if os.path.isfile(index_path):
post = Post(
index_path,
@@ -408,15 +505,18 @@ class Galleries(Task, ImageProcessor):
destination,
False,
self.site.MESSAGES,
- 'story.tmpl',
- self.site.get_compiler(index_path)
+ 'page.tmpl',
+ self.site.get_compiler(index_path),
+ None,
+ self.site.metadata_extractors_by
)
# If this did not exist, galleries without a title in the
# index.txt file would be errorneously named `index`
# (warning: galleries titled index and filenamed differently
# may break)
- if post.title == 'index':
- post.title = os.path.split(gallery)[1]
+ if post.title() == 'index':
+ for lang in post.meta.keys():
+ post.meta[lang]['title'] = os.path.split(gallery)[1]
# Register the post (via #2417)
self.site.post_per_input_file[index_path] = post
else:
@@ -428,8 +528,8 @@ class Galleries(Task, ImageProcessor):
exclude_path = os.path.join(gallery_path, "exclude.meta")
try:
- f = open(exclude_path, 'r')
- excluded_image_name_list = f.read().split()
+ with open(exclude_path, 'r') as f:
+ excluded_image_name_list = f.read().split()
except IOError:
excluded_image_name_list = []
@@ -473,34 +573,26 @@ class Galleries(Task, ImageProcessor):
orig_dest_path = os.path.join(output_gallery, img_name)
yield utils.apply_filters({
'basename': self.name,
- 'name': thumb_path,
- 'file_dep': [img],
- 'targets': [thumb_path],
- 'actions': [
- (self.resize_image,
- (img, thumb_path, self.kw['thumbnail_size'], False, self.kw['preserve_exif_data'],
- self.kw['exif_whitelist']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed({
- 1: self.kw['thumbnail_size']
- }, 'nikola.plugins.task.galleries:resize_thumb')],
- }, self.kw['filters'])
-
- yield utils.apply_filters({
- 'basename': self.name,
'name': orig_dest_path,
'file_dep': [img],
- 'targets': [orig_dest_path],
+ 'targets': [thumb_path, orig_dest_path],
'actions': [
(self.resize_image,
- (img, orig_dest_path, self.kw['max_image_size'], False, self.kw['preserve_exif_data'],
- self.kw['exif_whitelist']))
- ],
+ [img], {
+ 'dst_paths': [thumb_path, orig_dest_path],
+ 'max_sizes': [self.kw['thumbnail_size'], self.kw['max_image_size']],
+ 'bigger_panoramas': True,
+ 'preserve_exif_data': self.kw['preserve_exif_data'],
+ 'exif_whitelist': self.kw['exif_whitelist'],
+ 'preserve_icc_profiles': self.kw['preserve_icc_profiles']})],
'clean': True,
'uptodate': [utils.config_changed({
- 1: self.kw['max_image_size']
- }, 'nikola.plugins.task.galleries:resize_max')],
+ 1: self.kw['thumbnail_size'],
+ 2: self.kw['max_image_size'],
+ 3: self.kw['preserve_exif_data'],
+ 4: self.kw['exif_whitelist'],
+ 5: self.kw['preserve_icc_profiles'],
+ }, 'nikola.plugins.task.galleries:resize_thumb')],
}, self.kw['filters'])
def remove_excluded_image(self, img, input_folder):
@@ -546,7 +638,7 @@ class Galleries(Task, ImageProcessor):
img_list,
img_titles,
thumbs,
- file_dep):
+ img_metadata):
"""Build the gallery index."""
# The photo array needs to be created here, because
# it relies on thumbnails already being created on
@@ -568,7 +660,7 @@ class Galleries(Task, ImageProcessor):
else:
img_list, thumbs, img_titles = [], [], []
- photo_array = []
+ photo_info = OrderedDict()
for img, thumb, title in zip(img_list, thumbs, img_titles):
w, h = _image_size_cache.get(thumb, (None, None))
if w is None:
@@ -578,8 +670,11 @@ class Galleries(Task, ImageProcessor):
im = Image.open(thumb)
w, h = im.size
_image_size_cache[thumb] = w, h
- # Thumbs are files in output, we need URLs
- photo_array.append({
+ im.close()
+ # Use basename to avoid issues with multilingual sites (Issue #3078)
+ img_basename = os.path.basename(img)
+ photo_info[img_basename] = {
+ # Thumbs are files in output, we need URLs
'url': url_from_path(img),
'url_thumb': url_from_path(thumb),
'title': title,
@@ -587,9 +682,27 @@ class Galleries(Task, ImageProcessor):
'w': w,
'h': h
},
- })
+ 'width': w,
+ 'height': h
+ }
+ if img_basename in img_metadata:
+ photo_info[img_basename].update(img_metadata[img_basename])
+ photo_array = []
+ if context['order']:
+ for entry in context['order']:
+ photo_array.append(photo_info.pop(entry))
+ # Do we have any orphan entries from metadata.yml, or
+ # are the files from the gallery not listed in metadata.yml?
+ if photo_info:
+ for entry in photo_info:
+ photo_array.append(photo_info[entry])
+ else:
+ for entry in photo_info:
+ photo_array.append(photo_info[entry])
+
context['photo_array'] = photo_array
context['photo_array_json'] = json.dumps(photo_array, sort_keys=True)
+
self.site.render_template(template_name, output_name, context)
def gallery_rss(self, img_list, dest_img_list, img_titles, lang, permalink, output_path, title):
@@ -647,6 +760,6 @@ class Galleries(Task, ImageProcessor):
utils.makedirs(dst_dir)
with io.open(output_path, "w+", encoding="utf-8") as rss_file:
data = rss_obj.to_xml(encoding='utf-8')
- if isinstance(data, utils.bytes_str):
+ if isinstance(data, bytes):
data = data.decode('utf-8')
rss_file.write(data)
diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin
index d3a34ee..cc078b7 100644
--- a/nikola/plugins/task/gzip.plugin
+++ b/nikola/plugins/task/gzip.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create gzipped copies of files
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
index 79a11dc..ebd427f 100644
--- a/nikola/plugins/task/gzip.py
+++ b/nikola/plugins/task/gzip.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 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 553b5ad..f4a8f05 100644
--- a/nikola/plugins/task/indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_indexes
+name = classify_indexes
module = indexes
[Documentation]
@@ -9,5 +9,4 @@ website = https://getnikola.com/
description = Generates the blog's index pages.
[Nikola]
-plugincategory = Task
-
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py
index 8ecd1de..20491fb 100644
--- a/nikola/plugins/task/indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,323 +24,114 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the blog indexes."""
+"""Render the blog's main index."""
-from __future__ import unicode_literals
-from collections import defaultdict
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from nikola.plugin_categories import Task
-from nikola import utils
-from nikola.nikola import _enclosure
+from nikola.plugin_categories import Taxonomy
-class Indexes(Task):
- """Render the blog indexes."""
+class Indexes(Taxonomy):
+ """Classify for the blog's main index."""
- name = "render_indexes"
+ name = "classify_indexes"
- def set_site(self, site):
- """Set Nikola site."""
- self.number_of_pages = dict()
- self.number_of_pages_section = {lang: dict() for lang in site.config['TRANSLATIONS']}
- site.register_path_handler('index', self.index_path)
- site.register_path_handler('index_atom', self.index_atom_path)
- site.register_path_handler('section_index', self.index_section_path)
- site.register_path_handler('section_index_atom', self.index_section_atom_path)
- site.register_path_handler('section_index_rss', self.index_section_rss_path)
- return super(Indexes, self).set_site(site)
-
- def _get_filtered_posts(self, lang, show_untranslated_posts):
- """Return a filtered list of all posts for the given language.
-
- If show_untranslated_posts is True, will only include posts which
- are translated to the given language. Otherwise, returns all posts.
- """
- if show_untranslated_posts:
- return self.site.posts
- else:
- return [x for x in self.site.posts if x.is_translation_available(lang)]
-
- def _compute_number_of_pages(self, filtered_posts, posts_count):
- """Given a list of posts and the maximal number of posts per page, computes the number of pages needed."""
- return min(1, (len(filtered_posts) + posts_count - 1) // posts_count)
-
- def gen_tasks(self):
- """Render the blog indexes."""
- self.site.scan_posts()
- yield self.group_task()
-
- kw = {
- "translations": self.site.config['TRANSLATIONS'],
- "messages": self.site.MESSAGES,
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "filters": self.site.config['FILTERS'],
- "index_file": self.site.config['INDEX_FILE'],
- "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'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
- "blog_title": self.site.config["BLOG_TITLE"],
- "generate_atom": self.site.config["GENERATE_ATOM"],
- "site_url": self.site.config["SITE_URL"],
- }
-
- template_name = "index.tmpl"
- for lang in kw["translations"]:
- 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)
-
- filtered_posts = self._get_filtered_posts(lang, kw["show_untranslated_posts"])
-
- indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang)
- self.number_of_pages[lang] = self._compute_number_of_pages(filtered_posts, kw['index_display_post_count'])
-
- context = {}
- context["pagekind"] = ["main_index", "index"]
-
- yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path)
-
- if self.site.config['POSTS_SECTIONS']:
- index_len = len(kw['index_file'])
-
- groups = defaultdict(list)
- for p in filtered_posts:
- groups[p.section_slug(lang)].append(p)
-
- # don't build sections when there is only one, aka. default setups
- if not len(groups.items()) > 1:
- continue
-
- for section_slug, post_list in groups.items():
- self.number_of_pages_section[lang][section_slug] = self._compute_number_of_pages(post_list, kw['index_display_post_count'])
-
- def cat_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("section_index" + feed, section_slug, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- def cat_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("section_index" + feed, section_slug, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
+ classification_name = "index"
+ overview_page_variable_name = None
+ more_than_one_classifications_per_post = False
+ has_hierarchy = False
+ show_list_as_index = True
+ template_for_single_list = "index.tmpl"
+ template_for_classification_overview = None
+ apply_to_posts = True
+ apply_to_pages = False
+ omit_empty_classifications = False
+ path_handler_docstrings = {
+ 'index_index': False,
+ 'index': """Link to a numbered index.
- context = {}
+Example:
- short_destination = os.path.join(section_slug, kw['index_file'])
- link = short_destination.replace('\\', '/')
- if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
- link = link[:-index_len]
- context["permalink"] = link
- context["pagekind"] = ["section_page"]
- context["description"] = self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang)[section_slug] if section_slug in self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang) else ""
+link://index/3 => /index-3.html""",
+ 'index_atom': """Link to a numbered Atom index.
- if self.site.config["POSTS_SECTION_ARE_INDEXES"]:
- context["pagekind"].append("index")
- posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang)
+Example:
- section_title = None
- if type(posts_section_title) is dict:
- if section_slug in posts_section_title:
- section_title = posts_section_title[section_slug]
- elif type(posts_section_title) is str:
- section_title = posts_section_title
- if not section_title:
- section_title = post_list[0].section_name(lang)
- section_title = section_title.format(name=post_list[0].section_name(lang))
+link://index_atom/3 => /index-3.atom""",
+ 'index_rss': """A link to the RSS feed path.
- task = self.site.generic_index_renderer(lang, post_list, section_title, "sectionindex.tmpl", context, kw, self.name, cat_link, cat_path)
- else:
- context["pagekind"].append("list")
- output_name = os.path.join(kw['output_folder'], section_slug, kw['index_file'])
- task = self.site.generic_post_list_renderer(lang, post_list, output_name, "list.tmpl", kw['filters'], context)
- task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
- task['basename'] = self.name
- yield task
+Example:
- # RSS feed for section
- deps = []
- deps_uptodate = []
- if kw["show_untranslated_posts"]:
- posts = post_list[:kw['feed_length']]
- else:
- posts = [x for x in post_list if x.is_translation_available(lang)][:kw['feed_length']]
- for post in posts:
- deps += post.deps(lang)
- deps_uptodate += post.deps_uptodate(lang)
+link://rss => /blog/rss.xml""",
+ }
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link('section_index_rss', section_slug, lang).lstrip('/'))
- output_name = os.path.join(kw['output_folder'], self.site.path('section_index_rss', section_slug, lang).lstrip(os.sep))
- task = {
- 'basename': self.name,
- 'name': os.path.normpath(output_name),
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"](lang), kw["site_url"],
- context["description"], posts, output_name,
- kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url,
- _enclosure, kw["feed_links_append_query"]))],
-
- 'task_dep': ['render_posts'],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.indexes')] + deps_uptodate,
- }
- yield task
-
- if not self.site.config["PAGE_INDEX"]:
- return
+ def set_site(self, site):
+ """Set Nikola site."""
+ # Redirect automatically generated 'index_rss' path handler to 'rss' for compatibility with old rss plugin
+ site.register_path_handler('rss', lambda name, lang: site.path_handlers['index_rss'](name, lang))
+ site.path_handlers['rss'].__doc__ = """A link to the RSS feed path.
+
+Example:
+
+ link://rss => /blog/rss.xml
+ """.strip()
+ return super().set_site(site)
+
+ def get_implicit_classifications(self, lang):
+ """Return a list of classification strings which should always appear in posts_per_classification."""
+ return [""]
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ return [""]
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return self.site.config["BLOG_TITLE"](lang)
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ if dest_type == 'rss':
+ return [
+ self.site.config['RSS_PATH'](lang),
+ self.site.config['RSS_FILENAME_BASE'](lang)
+ ], 'auto'
+ if dest_type == 'feed':
+ return [
+ self.site.config['ATOM_PATH'](lang),
+ self.site.config['ATOM_FILENAME_BASE'](lang)
+ ], 'auto'
+ page_number = None
+ if dest_type == 'page':
+ # Interpret argument as page number
+ try:
+ page_number = int(classification)
+ except (ValueError, TypeError):
+ pass
+ return [self.site.config['INDEX_PATH'](lang)], 'always', page_number
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
kw = {
- "translations": self.site.config['TRANSLATIONS'],
- "post_pages": self.site.config["post_pages"],
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "filters": self.site.config['FILTERS'],
- "index_file": self.site.config['INDEX_FILE'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
+ "show_untranslated_posts": self.site.config["SHOW_UNTRANSLATED_POSTS"],
}
- template_name = "list.tmpl"
- index_len = len(kw['index_file'])
- for lang in kw["translations"]:
- # Need to group by folder to avoid duplicated tasks (Issue #758)
- # Group all pages by path prefix
- groups = defaultdict(list)
- for p in self.site.timeline:
- if not p.is_post:
- destpath = p.destination_path(lang)
- if destpath[-(1 + index_len):] == '/' + kw['index_file']:
- destpath = destpath[:-(1 + index_len)]
- dirname = os.path.dirname(destpath)
- groups[dirname].append(p)
- for dirname, post_list in groups.items():
- context = {}
- context["items"] = []
- 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('\\', '/')
- if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
- link = link[:-index_len]
- context["permalink"] = link
- context["pagekind"] = ["list"]
- if dirname == "/":
- context["pagekind"].append("front_page")
-
- for post in post_list:
- # If there is an index.html pending to be created from
- # a page, do not generate the PAGE_INDEX
- if post.destination_path(lang) == short_destination:
- should_render = False
- else:
- context["items"].append((post.title(lang),
- post.permalink(lang),
- None))
-
- if should_render:
- task = self.site.generic_post_list_renderer(lang, post_list,
- output_name,
- template_name,
- kw['filters'],
- context)
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
- task['basename'] = self.name
- yield task
-
- def index_path(self, name, lang, is_feed=False):
- """Link to a numbered index.
-
- Example:
-
- link://index/3 => /index-3.html
- """
- extension = None
- if is_feed:
- extension = ".atom"
- index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
- else:
- index_file = self.site.config['INDEX_FILE']
- if lang in self.number_of_pages:
- number_of_pages = self.number_of_pages[lang]
- else:
- number_of_pages = self._compute_number_of_pages(self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']), self.site.config['INDEX_DISPLAY_POST_COUNT'])
- self.number_of_pages[lang] = number_of_pages
- 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, number_of_pages, self.site),
- lang,
- self.site,
- extension=extension)
-
- def index_section_path(self, name, lang, is_feed=False, is_rss=False):
- """Link to the index for a section.
-
- Example:
-
- link://section_index/cars => /cars/index.html
- """
- extension = None
-
- if is_feed:
- extension = ".atom"
- index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
- elif is_rss:
- index_file = 'rss.xml'
- else:
- index_file = self.site.config['INDEX_FILE']
- if name in self.number_of_pages_section[lang]:
- number_of_pages = self.number_of_pages_section[lang][name]
- else:
- posts = [post for post in self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']) if post.section_slug(lang) == name]
- number_of_pages = self._compute_number_of_pages(posts, self.site.config['INDEX_DISPLAY_POST_COUNT'])
- self.number_of_pages_section[lang][name] = number_of_pages
- return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang],
- name,
- index_file] if _f],
- None,
- utils.get_displayed_page_number(None, number_of_pages, self.site),
- lang,
- self.site,
- extension=extension)
-
- def index_atom_path(self, name, lang):
- """Link to a numbered Atom index.
-
- Example:
-
- link://index_atom/3 => /index-3.atom
- """
- return self.index_path(name, lang, is_feed=True)
-
- def index_section_atom_path(self, name, lang):
- """Link to the Atom index for a section.
-
- Example:
-
- link://section_index_atom/cars => /cars/index.atom
- """
- return self.index_section_path(name, lang, is_feed=True)
+ context = {
+ "title": self.site.config["INDEXES_TITLE"](lang) or self.site.config["BLOG_TITLE"](lang),
+ "description": self.site.config["BLOG_DESCRIPTION"](lang),
+ "pagekind": ["main_index", "index"],
+ "featured": [p for p in self.site.posts if p.post_status == 'featured' and
+ (lang in p.translated_to or kw["show_untranslated_posts"])],
+ }
+ kw.update(context)
+ return context, kw
- def index_section_rss_path(self, name, lang):
- """Link to the RSS feed for a section.
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_INDEXES"]
- Example:
+ def should_generate_atom_for_classification_page(self, classification, post_list, lang):
+ """Only generates Atom feed for list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_MAIN_ATOM_FEED"]
- link://section_index_rss/cars => /cars/rss.xml
- """
- return self.index_section_path(name, lang, is_rss=True)
+ def should_generate_rss_for_classification_page(self, classification, post_list, lang):
+ """Only generates RSS feed for list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_MAIN_RSS_FEED"]
diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin
index 8fc2e2d..03b67d2 100644
--- a/nikola/plugins/task/listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Render code listings into output
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
index e694aa5..c946313 100644
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,12 @@
"""Render code listings."""
-from __future__ import unicode_literals, print_function
-
-from collections import defaultdict
import os
-import lxml.html
+from collections import defaultdict
+import natsort
from pygments import highlight
from pygments.lexers import get_lexer_for_filename, guess_lexer, TextLexer
-import natsort
from nikola.plugin_categories import Task
from nikola import utils
@@ -92,7 +89,7 @@ class Listings(Task):
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):
+ for root, _, 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:
@@ -104,7 +101,7 @@ class Listings(Task):
# Register file names in the mapping.
self.register_output_name(input_folder, rel_name, rel_output_name)
- return super(Listings, self).set_site(site)
+ return super().set_site(site)
def gen_tasks(self):
"""Render pretty code listings."""
@@ -115,24 +112,31 @@ class Listings(Task):
needs_ipython_css = False
if in_name and in_name.endswith('.ipynb'):
# Special handling: render ipynbs in listings (Issue #1900)
- ipynb_compiler = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler").plugin_object
- ipynb_raw = ipynb_compiler.compile_html_string(in_name, True)
- ipynb_html = lxml.html.fromstring(ipynb_raw)
- # The raw HTML contains garbage (scripts and styles), we can’t leave it in
- code = lxml.html.tostring(ipynb_html.xpath('//*[@id="notebook"]')[0], encoding='unicode')
+ ipynb_plugin = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler")
+ if ipynb_plugin is None:
+ msg = "To use .ipynb files as listings, you must set up the Jupyter compiler in COMPILERS and POSTS/PAGES."
+ utils.LOGGER.error(msg)
+ raise ValueError(msg)
+
+ ipynb_compiler = ipynb_plugin.plugin_object
+ with open(in_name, "r", encoding="utf-8-sig") as in_file:
+ nb_json = ipynb_compiler._nbformat_read(in_file)
+ code = ipynb_compiler._compile_string(nb_json)
title = os.path.basename(in_name)
needs_ipython_css = True
elif in_name:
- with open(in_name, 'r') as fd:
+ with open(in_name, 'r', encoding='utf-8-sig') as fd:
try:
lexer = get_lexer_for_filename(in_name)
- except:
+ except Exception:
try:
lexer = guess_lexer(fd.read())
- except:
+ except Exception:
lexer = TextLexer()
fd.seek(0)
- code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name))
+ code = highlight(
+ fd.read(), lexer,
+ utils.NikolaPygmentsHTML(in_name, linenos='table'))
title = os.path.basename(in_name)
else:
code = ''
@@ -184,7 +188,7 @@ class Listings(Task):
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
+ uptodate['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang'])
@@ -220,6 +224,8 @@ class Listings(Task):
'clean': True,
}, self.kw["filters"])
for f in files:
+ if f == '.DS_Store':
+ continue
ext = os.path.splitext(f)[-1]
if ext in ignored_extensions:
continue
@@ -257,7 +263,7 @@ class Listings(Task):
}, self.kw["filters"])
def listing_source_path(self, name, lang):
- """A link to the source code for a listing.
+ """Return a link to the source code for a listing.
It will try to use the file name if it's not ambiguous, or the file path.
@@ -273,7 +279,7 @@ class Listings(Task):
return result
def listing_path(self, namep, lang):
- """A link to a listing.
+ """Return a link to a listing.
It will try to use the file name if it's not ambiguous, or the file path.
@@ -297,7 +303,7 @@ class Listings(Task):
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])))
return ["ERROR"]
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.")
+ utils.LOGGER.warning("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
name = list(self.improper_input_file_mapping[name])[0]
break
else:
diff --git a/nikola/plugins/task/page_index.plugin b/nikola/plugins/task/page_index.plugin
new file mode 100644
index 0000000..42c9288
--- /dev/null
+++ b/nikola/plugins/task/page_index.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = classify_page_index
+module = page_index
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Generates the blog's index pages.
+
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/page_index.py b/nikola/plugins/task/page_index.py
new file mode 100644
index 0000000..e7b33cf
--- /dev/null
+++ b/nikola/plugins/task/page_index.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the page index."""
+
+
+from nikola.plugin_categories import Taxonomy
+
+
+class PageIndex(Taxonomy):
+ """Classify for the page index."""
+
+ name = "classify_page_index"
+
+ classification_name = "page_index_folder"
+ overview_page_variable_name = "page_folder"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = False
+ show_list_as_index = False
+ template_for_single_list = "list.tmpl"
+ template_for_classification_overview = None
+ always_disable_rss = True
+ always_disable_atom = True
+ apply_to_posts = False
+ apply_to_pages = True
+ omit_empty_classifications = True
+ path_handler_docstrings = {
+ 'page_index_folder_index': None,
+ 'page_index_folder': None,
+ 'page_index_folder_atom': None,
+ 'page_index_folder_rss': None,
+ }
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return self.site.config["PAGE_INDEX"]
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ destpath = post.destination_path(lang, sep='/')
+ if post.has_pretty_url(lang):
+ idx = '/index.html'
+ if destpath.endswith(idx):
+ destpath = destpath[:-len(idx)]
+ i = destpath.rfind('/')
+ return [destpath[:i] if i >= 0 else '']
+
+ def get_classification_friendly_name(self, dirname, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return dirname
+
+ def get_path(self, hierarchy, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ return hierarchy, 'always'
+
+ def extract_hierarchy(self, dirname):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return dirname.split('/') if dirname else []
+
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return '/'.join(hierarchy)
+
+ def provide_context_and_uptodate(self, dirname, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "filters": self.site.config['FILTERS'],
+ }
+ context = {
+ "title": self.site.config['BLOG_TITLE'](lang),
+ "pagekind": ["list", "front_page", "page_index"] if dirname == '' else ["list", "page_index"],
+ "kind": "page_index_folder",
+ "classification": dirname,
+ "has_no_feeds": True,
+ }
+ kw.update(context)
+ return context, kw
+
+ def should_generate_classification_page(self, dirname, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ short_destination = dirname + '/' + self.site.config['INDEX_FILE']
+ for post in post_list:
+ # If there is an index.html pending to be created from a page, do not generate the page index.
+ if post.destination_path(lang, sep='/') == short_destination:
+ return False
+ return True
diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin
index 1bdc7f4..a04cd05 100644
--- a/nikola/plugins/task/pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create pages in the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py
index 7d8287b..0c0bdd2 100644
--- a/nikola/plugins/task/pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,9 +26,10 @@
"""Render pages into output."""
-from __future__ import unicode_literals
+import os
+
from nikola.plugin_categories import Task
-from nikola.utils import config_changed
+from nikola.utils import config_changed, LOGGER
class RenderPages(Task):
@@ -47,6 +48,13 @@ class RenderPages(Task):
}
self.site.scan_posts()
yield self.group_task()
+ index_paths = {}
+ for lang in kw["translations"]:
+ index_paths[lang] = False
+ if not self.site.config["DISABLE_INDEXES"]:
+ index_paths[lang] = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'],
+ self.site.path('index', '', lang=lang)))
+
for lang in kw["translations"]:
for post in self.site.timeline:
if not kw["show_untranslated_posts"] and not post.is_translation_available(lang):
@@ -56,6 +64,12 @@ class RenderPages(Task):
else:
context = {'pagekind': ['story_page', 'page_page']}
for task in self.site.generic_page_renderer(lang, post, kw["filters"], context):
+ if task['name'] == index_paths[lang]:
+ # Issue 3022
+ LOGGER.error(
+ "Post {0!r}: output path ({1}) conflicts with the blog index ({2}). "
+ "Please change INDEX_PATH or disable index generation.".format(
+ post.source_path, task['name'], index_paths[lang]))
task['uptodate'] = task['uptodate'] + [config_changed(kw, 'nikola.plugins.task.pages')]
task['basename'] = self.name
task['task_dep'] = ['render_posts']
diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin
index c9578bc..6893472 100644
--- a/nikola/plugins/task/posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create HTML fragments out of posts.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py
index fe10c5f..5f48165 100644
--- a/nikola/plugins/task/posts.py
+++ b/nikola/plugins/task/posts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,11 +26,11 @@
"""Build HTML fragments from metadata and text."""
-from copy import copy
import os
+from copy import copy
from nikola.plugin_categories import Task
-from nikola import filters, utils
+from nikola import utils
def update_deps(post, lang, task):
@@ -85,11 +85,12 @@ class RenderPosts(Task):
deps_dict[k] = self.site.config.get(k)
dest = post.translated_base_path(lang)
file_dep = [p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")]
+ extra_targets = post.compiler.get_extra_targets(post, lang, dest)
task = {
'basename': self.name,
'name': dest,
'file_dep': file_dep,
- 'targets': [dest],
+ 'targets': [dest] + extra_targets,
'actions': [(post.compile, (lang, )),
(update_deps, (post, lang, )),
],
@@ -107,12 +108,9 @@ class RenderPosts(Task):
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
+ _f = self.site.filters.get(f)
+ if _f is not None: # A registered filter
+ flist.append(_f)
else:
flist.append(f)
yield utils.apply_filters(task, {os.path.splitext(dest)[-1]: flist})
diff --git a/nikola/plugins/task/py3_switch.plugin b/nikola/plugins/task/py3_switch.plugin
deleted file mode 100644
index b0014e1..0000000
--- a/nikola/plugins/task/py3_switch.plugin
+++ /dev/null
@@ -1,13 +0,0 @@
-[Core]
-name = py3_switch
-module = py3_switch
-
-[Documentation]
-author = Roberto Alsina
-version = 1.0
-website = https://getnikola.com/
-description = Beg the user to switch to Python 3
-
-[Nikola]
-plugincategory = Task
-
diff --git a/nikola/plugins/task/py3_switch.py b/nikola/plugins/task/py3_switch.py
deleted file mode 100644
index 2ff4e2d..0000000
--- a/nikola/plugins/task/py3_switch.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Beg the user to switch to python 3."""
-
-import datetime
-import os
-import random
-import sys
-
-import doit.tools
-
-from nikola.utils import get_logger, STDERR_HANDLER
-from nikola.plugin_categories import LateTask
-
-PY2_AND_NO_PY3_WARNING = """Nikola is going to deprecate Python 2 support in 2016. Your current
-version will continue to work, but please consider upgrading to Python 3.
-
-Please check http://bit.ly/1FKEsiX for details.
-"""
-PY2_WARNING = """Nikola is going to deprecate Python 2 support in 2016. You already have Python 3
-available in your system. Why not switch?
-
-Please check http://bit.ly/1FKEsiX for details.
-"""
-PY2_BARBS = [
- "Python 2 has been deprecated for years. Stop clinging to your long gone youth and switch to Python3.",
- "Python 2 is the safety blanket of languages. Be a big kid and switch to Python 3",
- "Python 2 is old and busted. Python 3 is the new hotness.",
- "Nice unicode you have there, would be a shame something happened to it.. switch to python 3!.",
- "Don't get in the way of progress! Upgrade to Python 3 and save a developer's mind today!",
- "Winners don't use Python 2 -- Signed: The FBI",
- "Python 2? What year is it?",
- "I just wanna tell you how I'm feeling\n"
- "Gotta make you understand\n"
- "Never gonna give you up [But Python 2 has to go]",
- "The year 2009 called, and they want their Python 2.7 back.",
-]
-
-
-LOGGER = get_logger('Nikola', STDERR_HANDLER)
-
-
-def has_python_3():
- """Check if python 3 is available."""
- if 'win' in sys.platform:
- py_bin = 'py.exe'
- else:
- py_bin = 'python3'
- for path in os.environ["PATH"].split(os.pathsep):
- if os.access(os.path.join(path, py_bin), os.X_OK):
- return True
- return False
-
-
-class Py3Switch(LateTask):
- """Beg the user to switch to python 3."""
-
- name = "_switch to py3"
-
- def gen_tasks(self):
- """Beg the user to switch to python 3."""
- def give_warning():
- if sys.version_info[0] == 3:
- return
- if has_python_3():
- LOGGER.warn(random.choice(PY2_BARBS))
- LOGGER.warn(PY2_WARNING)
- else:
- LOGGER.warn(PY2_AND_NO_PY3_WARNING)
-
- task = {
- 'basename': self.name,
- 'name': 'please!',
- 'actions': [give_warning],
- 'clean': True,
- 'uptodate': [doit.tools.timeout(datetime.timedelta(days=3))]
- }
-
- return task
diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin
index c5a3042..57bd0c0 100644
--- a/nikola/plugins/task/redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create redirect pages.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py
index b170b81..a89fbd0 100644
--- a/nikola/plugins/task/redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Generate redirections."""
-from __future__ import unicode_literals
import os
@@ -45,12 +44,15 @@ class Redirect(Task):
'redirections': self.site.config['REDIRECTIONS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
'filters': self.site.config['FILTERS'],
+ 'index_file': self.site.config['INDEX_FILE'],
}
yield self.group_task()
if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src.lstrip('/'))
+ if src_path.endswith("/"):
+ src_path += kw['index_file']
yield utils.apply_filters({
'basename': self.name,
'name': src_path,
diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin
index 7ae56c6..51f7781 100644
--- a/nikola/plugins/task/robots.plugin
+++ b/nikola/plugins/task/robots.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generate /robots.txt exclusion file and promote sitemap.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py
index 8537fc8..627d436 100644
--- a/nikola/plugins/task/robots.py
+++ b/nikola/plugins/task/robots.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,13 +26,9 @@
"""Generate a robots.txt file."""
-from __future__ import print_function, absolute_import, unicode_literals
import io
import os
-try:
- from urlparse import urljoin, urlparse
-except ImportError:
- from urllib.parse import urljoin, urlparse # NOQA
+from urllib.parse import urljoin, urlparse
from nikola.plugin_categories import LateTask
from nikola import utils
@@ -59,7 +55,8 @@ class RobotsFile(LateTask):
def write_robots():
if kw["site_url"] != urljoin(kw["site_url"], "/"):
- utils.LOGGER.warn('robots.txt not ending up in server root, will be useless')
+ utils.LOGGER.warning('robots.txt not ending up in server root, will be useless')
+ utils.LOGGER.info('Add "robots" to DISABLED_PLUGINS to disable this warning and robots.txt generation.')
with io.open(robots_path, 'w+', encoding='utf8') as outf:
outf.write("Sitemap: {0}\n\n".format(sitemapindex_url))
@@ -82,6 +79,6 @@ class RobotsFile(LateTask):
"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 file.')
+ utils.LOGGER.warning('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied file.')
else:
utils.LOGGER.debug('Did not generate robots.txt as one already exists in FILES_FOLDERS.')
diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin
deleted file mode 100644
index 4dd8aba..0000000
--- a/nikola/plugins/task/rss.plugin
+++ /dev/null
@@ -1,13 +0,0 @@
-[Core]
-name = generate_rss
-module = rss
-
-[Documentation]
-author = Roberto Alsina
-version = 1.0
-website = https://getnikola.com/
-description = Generate RSS feeds.
-
-[Nikola]
-plugincategory = Task
-
diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py
deleted file mode 100644
index 780559b..0000000
--- a/nikola/plugins/task/rss.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Generate RSS feeds."""
-
-from __future__ import unicode_literals, print_function
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-
-from nikola import utils
-from nikola.nikola import _enclosure
-from nikola.plugin_categories import Task
-
-
-class GenerateRSS(Task):
- """Generate RSS feeds."""
-
- name = "generate_rss"
-
- def set_site(self, site):
- """Set Nikola site."""
- site.register_path_handler('rss', self.rss_path)
- return super(GenerateRSS, self).set_site(site)
-
- def gen_tasks(self):
- """Generate RSS feeds."""
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "filters": self.site.config["FILTERS"],
- "blog_title": self.site.config["BLOG_TITLE"],
- "site_url": self.site.config["SITE_URL"],
- "base_url": self.site.config["BASE_URL"],
- "blog_description": self.site.config["BLOG_DESCRIPTION"],
- "output_folder": self.site.config["OUTPUT_FOLDER"],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "feed_previewimage": self.site.config["FEED_PREVIEWIMAGE"],
- "tzinfo": self.site.tzinfo,
- "feed_read_more_link": self.site.config["FEED_READ_MORE_LINK"],
- "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- }
- self.site.scan_posts()
- # Check for any changes in the state of use_in_feeds for any post.
- # Issue #934
- kw['use_in_feeds_status'] = ''.join(
- ['T' if x.use_in_feeds else 'F' for x in self.site.timeline]
- )
- yield self.group_task()
- for lang in kw["translations"]:
- 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[:kw['feed_length']]
- else:
- 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('/'))
-
- task = {
- 'basename': 'generate_rss',
- 'name': os.path.normpath(output_name),
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"](lang), kw["site_url"],
- kw["blog_description"](lang), posts, output_name,
- kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url,
- _enclosure, kw["feed_links_append_query"]))],
-
- 'task_dep': ['render_posts'],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.rss')] + deps_uptodate,
- }
- yield utils.apply_filters(task, kw['filters'])
-
- def rss_path(self, name, lang):
- """A link to the RSS feed path.
-
- Example:
-
- link://rss => /blog/rss.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['RSS_PATH'], 'rss.xml'] if _f]
diff --git a/nikola/plugins/task/scale_images.plugin b/nikola/plugins/task/scale_images.plugin
index 3edd0c6..332f583 100644
--- a/nikola/plugins/task/scale_images.plugin
+++ b/nikola/plugins/task/scale_images.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create down-scaled images and thumbnails.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py
index 2b483ae..fa3a67b 100644
--- a/nikola/plugins/task/scale_images.py
+++ b/nikola/plugins/task/scale_images.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2014-2016 Pelle Nilsson and others.
+# Copyright © 2014-2020 Pelle Nilsson and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -38,29 +38,24 @@ class ScaleImage(Task, ImageProcessor):
name = "scale_images"
- def set_site(self, site):
- """Set Nikola site."""
- self.logger = utils.get_logger('scale_images', utils.STDERR_HANDLER)
- return super(ScaleImage, self).set_site(site)
-
def process_tree(self, src, dst):
"""Process all images in a src tree and put the (possibly) rescaled images in the dst folder."""
- ignore = set(['.svn'])
+ thumb_fmt = self.kw['image_thumbnail_format']
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))
+ thumb_name, thumb_ext = os.path.splitext(src_name)
+ thumb_file = os.path.join(dst_dir, thumb_fmt.format(
+ name=thumb_name,
+ ext=thumb_ext,
+ ))
yield {
'name': dst_file,
'file_dep': [src_file],
@@ -71,19 +66,28 @@ class ScaleImage(Task, ImageProcessor):
def process_image(self, src, dst, thumb):
"""Resize an image."""
- self.resize_image(src, dst, self.kw['max_image_size'], False, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'])
- self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'])
+ self.resize_image(
+ src,
+ dst_paths=[dst, thumb],
+ max_sizes=[self.kw['max_image_size'], self.kw['image_thumbnail_size']],
+ bigger_panoramas=True,
+ preserve_exif_data=self.kw['preserve_exif_data'],
+ exif_whitelist=self.kw['exif_whitelist'],
+ preserve_icc_profiles=self.kw['preserve_icc_profiles']
+ )
def gen_tasks(self):
"""Copy static files into the output folder."""
self.kw = {
'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'],
+ 'image_thumbnail_format': self.site.config['IMAGE_THUMBNAIL_FORMAT'],
'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'],
'preserve_exif_data': self.site.config['PRESERVE_EXIF_DATA'],
'exif_whitelist': self.site.config['EXIF_WHITELIST'],
+ 'preserve_icc_profiles': self.site.config['PRESERVE_ICC_PROFILES'],
}
self.image_ext_list = self.image_ext_list_builtin
diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index 83e72c4..c8aa832 100644
--- a/nikola/plugins/task/sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generate google sitemap.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap.py
index 64fcb45..8bbaa63 100644
--- a/nikola/plugins/task/sitemap/__init__.py
+++ b/nikola/plugins/task/sitemap.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,18 +26,13 @@
"""Generate a sitemap."""
-from __future__ import print_function, absolute_import, unicode_literals
-import io
import datetime
-import dateutil.tz
+import io
import os
-import sys
-try:
- from urlparse import urljoin, urlparse
- import robotparser as robotparser
-except ImportError:
- from urllib.parse import urljoin, urlparse # NOQA
- import urllib.robotparser as robotparser # NOQA
+import urllib.robotparser as robotparser
+from urllib.parse import urljoin, urlparse
+
+import dateutil.tz
from nikola.plugin_categories import LateTask
from nikola.utils import apply_filters, config_changed, encodelink
@@ -119,7 +114,6 @@ class Sitemap(LateTask):
"output_folder": self.site.config["OUTPUT_FOLDER"],
"strip_indexes": self.site.config["STRIP_INDEXES"],
"index_file": self.site.config["INDEX_FILE"],
- "sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"],
"mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.php', '.xml', '.rss']),
"robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
"filters": self.site.config["FILTERS"],
@@ -142,18 +136,19 @@ class Sitemap(LateTask):
def scan_locs():
"""Scan site locations."""
for root, dirs, files in os.walk(output, followlinks=True):
- if not dirs and not files and not kw['sitemap_include_fileless_dirs']:
+ if not dirs and not files:
continue # Totally empty, not on sitemap
path = os.path.relpath(root, output)
# ignore the current directory.
if path == '.':
- path = ''
+ path = syspath = ''
else:
+ syspath = path + os.sep
path = path.replace(os.sep, '/') + '/'
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
- post = self.site.post_per_file.get(path + kw['index_file'])
+ post = self.site.post_per_file.get(syspath + kw['index_file'])
if post and (post.is_draft or post.is_private or post.publish_later):
continue
alternates = []
@@ -169,7 +164,7 @@ class Sitemap(LateTask):
continue # We already mapped the folder
if os.path.splitext(fname)[-1] in mapped_exts:
real_path = os.path.join(root, fname)
- path = os.path.relpath(real_path, output)
+ path = syspath = os.path.relpath(real_path, output)
if path.endswith(kw['index_file']) and kw['strip_indexes']:
# ignore index files when stripping urls
continue
@@ -177,16 +172,15 @@ class Sitemap(LateTask):
continue
# read in binary mode to make ancient files work
- fh = open(real_path, 'rb')
- filehead = fh.read(1024)
- fh.close()
+ with open(real_path, 'rb') as fh:
+ filehead = fh.read(1024)
if path.endswith('.html') or path.endswith('.htm') or path.endswith('.php'):
- """ ignores "html" files without doctype """
+ # Ignores "html" files without doctype
if b'<!doctype html' not in filehead.lower():
continue
- """ ignores "html" files with noindex robot directives """
+ # 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',
@@ -207,7 +201,7 @@ class Sitemap(LateTask):
continue
else:
continue # ignores all XML files except those presumed to be RSS
- post = self.site.post_per_file.get(path)
+ post = self.site.post_per_file.get(syspath)
if post and (post.is_draft or post.is_private or post.publish_later):
continue
path = path.replace(os.sep, '/')
@@ -227,12 +221,8 @@ class Sitemap(LateTask):
for rule in kw["robots_exclusions"]:
robot = robotparser.RobotFileParser()
robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)])
- if sys.version_info[0] == 3:
- if not robot.can_fetch("*", '/' + path):
- return False # not robot food
- else:
- if not robot.can_fetch("*", ('/' + path).encode('utf-8')):
- return False # not robot food
+ if not robot.can_fetch("*", '/' + path):
+ return False # not robot food
return True
def write_sitemap():
@@ -322,6 +312,7 @@ class Sitemap(LateTask):
lastmod = datetime.datetime.utcfromtimestamp(os.stat(p).st_mtime).replace(tzinfo=dateutil.tz.gettz('UTC'), second=0, microsecond=0).isoformat().replace('+00:00', 'Z')
return lastmod
+
if __name__ == '__main__':
import doctest
doctest.testmod()
diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin
index 66856f1..1ab1a3c 100644
--- a/nikola/plugins/task/sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy page sources into the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py
index 0d77aba..1d36429 100644
--- a/nikola/plugins/task/sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -61,12 +61,8 @@ class Sources(Task):
# do not publish PHP sources
if post.source_ext(True) == post.compiler.extension():
continue
- source = post.source_path
- if lang != kw["default_lang"]:
- source_lang = utils.get_translation_candidate(self.site.config, source, lang)
- if os.path.exists(source_lang):
- source = source_lang
- if os.path.isfile(source):
+ source = post.translated_source_path(lang)
+ if source is not None and os.path.isfile(source):
yield {
'basename': 'render_sources',
'name': os.path.normpath(output_name),
diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin
index c3a5be3..c17b7b3 100644
--- a/nikola/plugins/task/tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_tags
+name = classify_tags
module = tags
[Documentation]
@@ -9,5 +9,4 @@ website = https://getnikola.com/
description = Render the tag pages and feeds.
[Nikola]
-plugincategory = Task
-
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py
index 8b4683e..aecf8f5 100644
--- a/nikola/plugins/task/tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,497 +24,137 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the tag/category pages and feeds."""
+"""Render the tag pages and feeds."""
-from __future__ import unicode_literals
-import json
-import os
-import natsort
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from nikola.plugin_categories import Task
+from nikola.plugin_categories import Taxonomy
from nikola import utils
-from nikola.nikola import _enclosure
-class RenderTags(Task):
- """Render the tag/category pages and feeds."""
+class ClassifyTags(Taxonomy):
+ """Classify the posts by tags."""
- name = "render_tags"
+ name = "classify_tags"
- def set_site(self, site):
- """Set Nikola 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)
-
- def gen_tasks(self):
- """Render the tag pages and feeds."""
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "blog_title": self.site.config["BLOG_TITLE"],
- "site_url": self.site.config["SITE_URL"],
- "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'],
- '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'],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "feed_link_append_query": self.site.config["FEED_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'],
- "category_pages_descriptions": self.site.config['CATEGORY_PAGES_DESCRIPTIONS'],
- "category_pages_titles": self.site.config['CATEGORY_PAGES_TITLES'],
- "tag_pages_descriptions": self.site.config['TAG_PAGES_DESCRIPTIONS'],
- "tag_pages_titles": self.site.config['TAG_PAGES_TITLES'],
- }
-
- self.site.scan_posts()
- yield self.group_task()
-
- yield self.list_tags_page(kw)
-
- if not self.site.posts_per_tag and not self.site.posts_per_category:
- return
-
- for lang in kw["translations"]:
- if kw['category_path'][lang] == kw['tag_path'][lang]:
- tags = {self.slugify_tag_name(tag, lang): tag for tag in self.site.tags_per_language[lang]}
- cats = {tuple(self.slugify_category_name(category, lang)): 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}' for language {3}!".format('/'.join(categories[slug]), tags[slug], slug, lang))
-
- # Test for category slug clashes
- categories = {}
- for category in self.site.posts_per_category.keys():
- slug = tuple(self.slugify_category_name(category, lang))
- for part in slug:
- if len(part) == 0:
- utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- if slug in categories:
- other_category = categories[slug]
- utils.LOGGER.error('You have categories that are too similar: {0} and {1} (language {2})'.format(category, other_category, lang))
- 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]])))
- raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- categories[slug] = category
-
- tag_list = list(self.site.posts_per_tag.items())
- cat_list = list(self.site.posts_per_category.items())
-
- def render_lists(tag, posts, is_category=True):
- """Render tag pages as RSS files and lists/indexes."""
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for lang in kw["translations"]:
- if kw["show_untranslated_posts"]:
- filtered_posts = post_list
- else:
- filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
- if kw["generate_rss"]:
- yield self.tag_rss(tag, lang, filtered_posts, kw, is_category)
- # Render HTML
- 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)
-
- for tag, posts in tag_list:
- for task in render_lists(tag, posts, False):
- yield task
-
- 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.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(
- 'tag', tag, self.site.config['DEFAULT_LANG']), tag_posts]
- output_name = os.path.join(kw['output_folder'],
- 'assets', 'js', 'tag_cloud_data.json')
+ classification_name = "tag"
+ overview_page_variable_name = "tags"
+ overview_page_items_variable_name = "items"
+ more_than_one_classifications_per_post = True
+ has_hierarchy = False
+ show_list_as_subcategories_list = False
+ template_for_classification_overview = "tags.tmpl"
+ always_disable_rss = False
+ always_disable_atom = False
+ apply_to_posts = True
+ apply_to_pages = False
+ omit_empty_classifications = True
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'tag_index': """A link to the tag index.
- def write_tag_data(data):
- """Write tag data into JSON file, for use in tag clouds."""
- utils.makedirs(os.path.dirname(output_name))
- with open(output_name, 'w+') as fd:
- json.dump(data, fd, sort_keys=True)
+Example:
- if self.site.config['WRITE_TAG_CLOUD']:
- task = {
- 'basename': str(self.name),
- 'name': str(output_name)
- }
+link://tag_index => /tags/index.html""",
+ 'tag': """A link to a tag's page. Takes page number as optional keyword argument.
- 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'])
+Example:
- def _create_tags_page(self, kw, lang, include_tags=True, include_categories=True):
- """Create a global "all your tags/categories" page for each language."""
- categories = [cat.category_name for cat in self.site.category_hierarchy]
- has_categories = (categories != []) and include_categories
- template_name = "tags.tmpl"
- kw = kw.copy()
- if include_categories:
- kw['categories'] = categories
- tags = natsort.natsorted([tag for tag in self.site.tags_per_language[lang]
- if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]],
- alg=natsort.ns.F | natsort.ns.IC)
- has_tags = (tags != []) and include_tags
- if include_tags:
- kw['tags'] = tags
- output_name = os.path.join(
- kw['output_folder'], self.site.path('tag_index' if has_tags else 'category_index', None, lang))
- context = {}
- 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"]
- 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" if has_tags else "category_index", None, lang)
- context["description"] = context["title"]
- context["pagekind"] = ["list", "tags_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- 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):
- """Create a global "all your tags/categories" page for each language."""
- for lang in kw["translations"]:
- if self.site.config['TAG_PATH'][lang] == self.site.config['CATEGORY_PATH'][lang]:
- yield self._create_tags_page(kw, lang, True, True)
- else:
- yield self._create_tags_page(kw, lang, False, True)
- yield self._create_tags_page(kw, lang, 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_indexes_title(self, tag, nice_tag, is_category, lang, messages):
- titles = self.site.config['CATEGORY_PAGES_TITLES'] if is_category else self.site.config['TAG_PAGES_TITLES']
- return titles[lang][tag] if lang in titles and tag in titles[lang] else messages[lang]["Posts about %s"] % nice_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]
+link://tag/cats => /tags/cats.html""",
+ 'tag_atom': """A link to a tag's Atom feed.
- 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"
+Example:
- 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)
+link://tag_atom/cats => /tags/cats.atom""",
+ 'tag_rss': """A link to a tag's RSS feed.
- 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)
+Example:
- 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" """
- """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 = self._get_indexes_title(tag, title, is_category, lang, kw["messages"])
- context_source["description"] = self._get_description(tag, is_category, lang)
- if is_category:
- context_source["subcategories"] = self._get_subcategories(tag)
- context_source["pagekind"] = ["index", "tag_page"]
- template_name = "tagindex.tmpl"
+link://tag_rss/cats => /tags/cats.xml""",
+ }
- 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):
- """Render a single flat link list with this tag's posts."""
- kind = "category" if is_category else "tag"
- template_name = "tag.tmpl"
- output_name = os.path.join(kw['output_folder'], self.site.path(
- kind, tag, lang))
- context = {}
- context["lang"] = lang
- 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"] = self._get_indexes_title(tag, title, is_category, lang, kw["messages"])
- context["posts"] = post_list
- context["permalink"] = self.site.link(kind, tag, lang)
- context["kind"] = kind
- context["description"] = self._get_description(tag, is_category, lang)
- if is_category:
- context["subcategories"] = self._get_subcategories(tag)
- context["pagekind"] = ["list", "tag_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')]
- task['basename'] = str(self.name)
- yield task
-
- if self.site.config['GENERATE_ATOM']:
- yield self.atom_feed_list(kind, tag, lang, post_list, context, kw)
+ def set_site(self, site):
+ """Set site, which is a Nikola instance."""
+ super().set_site(site)
+ self.show_list_as_index = self.site.config['TAG_PAGES_ARE_INDEXES']
+ self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl"
+ self.minimum_post_count_per_classification_in_overview = self.site.config['TAGLIST_MINIMUM_POSTS']
+ self.translation_manager = utils.ClassificationTranslationManager()
- def atom_feed_list(self, kind, tag, lang, post_list, context, kw):
- """Generate atom feeds for tag lists."""
- if kind == 'tag':
- context['feedlink'] = self.site.abs_link(self.site.path('tag_atom', tag, lang))
- feed_path = os.path.join(kw['output_folder'], self.site.path('tag_atom', tag, lang))
- elif kind == 'category':
- context['feedlink'] = self.site.abs_link(self.site.path('category_atom', tag, lang))
- feed_path = os.path.join(kw['output_folder'], self.site.path('category_atom', tag, lang))
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return True
- task = {
- 'basename': str(self.name),
- 'name': feed_path,
- 'targets': [feed_path],
- 'actions': [(self.site.atom_feed_renderer, (lang, post_list, feed_path, kw['filters'], context))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:atom')],
- 'task_dep': ['render_posts'],
- }
- return task
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ return post.tags_for_language(lang)
- def tag_rss(self, tag, lang, posts, kw, is_category):
- """Create a RSS feed for a single tag in a given language."""
- kind = "category" if is_category else "tag"
- # Render RSS
- output_name = os.path.normpath(
- os.path.join(kw['output_folder'],
- self.site.path(kind + "_rss", tag, lang)))
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/'))
- deps = []
- deps_uptodate = []
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for post in post_list:
- deps += post.deps(lang)
- 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), self._get_title(tag, is_category)),
- kw["site_url"], None, post_list,
- output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
- feed_url, _enclosure, kw["feed_link_append_query"]))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate,
- 'task_dep': ['render_posts'],
- }
- return utils.apply_filters(task, kw['filters'])
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return classification
def slugify_tag_name(self, name, lang):
"""Slugify a tag name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderTags.slugify_tag_name() called without language!")
- lang = ''
if self.site.config['SLUG_TAG_PATH']:
name = utils.slugify(name, lang)
return name
- def tag_index_path(self, name, lang):
- """A link to the tag index.
-
- Example:
-
- link://tag_index => /tags/index.html
- """
- if self.site.config['TAGS_INDEX_PATH'][lang]:
- paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAGS_INDEX_PATH'][lang]] if _f]
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ if self.site.config['TAGS_INDEX_PATH'](lang):
+ path = self.site.config['TAGS_INDEX_PATH'](lang)
+ append_index = 'never'
else:
- paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.site.config['INDEX_FILE']] if _f]
- return paths
-
- def category_index_path(self, name, lang):
- """A link to the category index.
-
- Example:
-
- link://category_index => /categories/index.html
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang],
- self.site.config['INDEX_FILE']] if _f]
-
- def tag_path(self, name, lang):
- """A link to a tag's page.
-
- Example:
-
- link://tag/cats => /tags/cats.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.slugify_tag_name(name, lang),
- self.site.config['INDEX_FILE']] if _f]
- else:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.slugify_tag_name(name, lang) + ".html"] if _f]
-
- def tag_atom_path(self, name, lang):
- """A link to a tag's Atom feed.
-
- Example:
-
- link://tag_atom/cats => /tags/cats.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".atom"] if
- _f]
-
- def tag_rss_path(self, name, lang):
- """A link to a tag's RSS feed.
-
- Example:
-
- link://tag_rss/cats => /tags/cats.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".xml"] if
- _f]
-
- def slugify_category_name(self, name, lang):
- """Slugify a category name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderTags.slugify_category_name() called without language!")
- lang = ''
- 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, lang) 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):
- """A link to a category.
-
- Example:
-
- link://category/dogs => /categories/dogs.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self.slugify_category_name(name, lang) + [self.site.config['INDEX_FILE']]
- else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".html")
-
- def category_atom_path(self, name, lang):
- """A link to a category's Atom feed.
-
- Example:
-
- link://category_atom/dogs => /categories/dogs.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".atom")
+ path = self.site.config['TAG_PATH'](lang)
+ append_index = 'always'
+ return [component for component in path.split('/') if component], append_index
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ return [_f for _f in [
+ self.site.config['TAG_PATH'](lang),
+ self.slugify_tag_name(classification, lang)] if _f], 'auto'
+
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ "tag_path": self.site.config['TAG_PATH'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
+ "tzinfo": self.site.tzinfo,
+ "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'],
+ "tag_titles": self.site.config['TAG_TITLES'],
+ }
+ context = {
+ "title": self.site.MESSAGES[lang]["Tags"],
+ "description": self.site.MESSAGES[lang]["Tags"],
+ "pagekind": ["list", "tags_page"],
+ }
+ kw.update(context)
+ return context, kw
- def category_rss_path(self, name, lang):
- """A link to a category's RSS feed.
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ kw = {
+ "tag_path": self.site.config['TAG_PATH'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
+ "tzinfo": self.site.tzinfo,
+ "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'],
+ "tag_titles": self.site.config['TAG_TITLES'],
+ }
+ context = {
+ "title": self.site.config['TAG_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % classification),
+ "description": self.site.config['TAG_DESCRIPTIONS'].get(lang, {}).get(classification),
+ "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"],
+ "tag": classification,
+ }
+ kw.update(context)
+ return context, kw
- Example:
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same tag in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
- link://category_rss/dogs => /categories/dogs.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".xml")
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ self.translation_manager.read_from_config(self.site, 'TAG', posts_per_classification_per_language, False)
diff --git a/nikola/plugins/task/taxonomies.plugin b/nikola/plugins/task/taxonomies.plugin
new file mode 100644
index 0000000..5bda812
--- /dev/null
+++ b/nikola/plugins/task/taxonomies.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = render_taxonomies
+module = taxonomies
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Render the taxonomy overviews, classification pages and feeds.
+
+[Nikola]
+PluginCategory = Task
diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py
new file mode 100644
index 0000000..7dcf6ed
--- /dev/null
+++ b/nikola/plugins/task/taxonomies.py
@@ -0,0 +1,459 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the taxonomy overviews, classification pages and feeds."""
+
+import os
+from collections import defaultdict
+from copy import copy
+from urllib.parse import urljoin
+
+import blinker
+import natsort
+
+from nikola import utils, hierarchy_utils
+from nikola.nikola import _enclosure
+from nikola.plugin_categories import Task
+
+
+class RenderTaxonomies(Task):
+ """Render taxonomy pages and feeds."""
+
+ name = "render_taxonomies"
+
+ def _generate_classification_overview_kw_context(self, taxonomy, lang):
+ """Create context and kw for a classification overview page."""
+ context, kw = taxonomy.provide_overview_context_and_uptodate(lang)
+
+ context = copy(context)
+ context["kind"] = "{}_index".format(taxonomy.classification_name)
+ sorted_links = []
+ for other_lang in sorted(self.site.config['TRANSLATIONS'].keys()):
+ if other_lang != lang:
+ sorted_links.append((other_lang, None, None))
+ # Put the current language in front, so that it appears first in links
+ # (Issue #3248)
+ sorted_links_all = [(lang, None, None)] + sorted_links
+ context['has_other_languages'] = True
+ context['other_languages'] = sorted_links
+ context['all_languages'] = sorted_links_all
+
+ kw = copy(kw)
+ kw["messages"] = self.site.MESSAGES
+ kw["translations"] = self.site.config['TRANSLATIONS']
+ kw["filters"] = self.site.config['FILTERS']
+ kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview
+ kw["output_folder"] = self.site.config['OUTPUT_FOLDER']
+ kw["pretty_urls"] = self.site.config['PRETTY_URLS']
+ kw["strip_indexes"] = self.site.config['STRIP_INDEXES']
+ kw["index_file"] = self.site.config['INDEX_FILE']
+
+ # Collect all relevant classifications
+ if taxonomy.has_hierarchy:
+ def acceptor(node):
+ return len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang)) >= kw["minimum_post_count"]
+
+ clipped_root_list = [hierarchy_utils.clone_treenode(node, parent=None, acceptor=acceptor) for node in self.site.hierarchy_per_classification[taxonomy.classification_name][lang]]
+ clipped_root_list = [node for node in clipped_root_list if node]
+ clipped_flat_hierarchy = hierarchy_utils.flatten_tree_structure(clipped_root_list)
+
+ classifications = [cat.classification_name for cat in clipped_flat_hierarchy]
+ else:
+ classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items()
+ if len(self._filter_list(posts, lang)) >= kw["minimum_post_count"]],
+ alg=natsort.ns.F | natsort.ns.IC)
+ taxonomy.sort_classifications(classifications, lang)
+
+ # Set up classifications in context
+ context[taxonomy.overview_page_variable_name] = classifications
+ context["has_hierarchy"] = taxonomy.has_hierarchy
+ if taxonomy.overview_page_items_variable_name:
+ items = [(classification,
+ self.site.link(taxonomy.classification_name, classification, lang))
+ for classification in classifications]
+ items_with_postcount = [
+ (classification,
+ self.site.link(taxonomy.classification_name, classification, lang),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][classification], lang)))
+ for classification in classifications
+ ]
+ context[taxonomy.overview_page_items_variable_name] = items
+ context[taxonomy.overview_page_items_variable_name + "_with_postcount"] = items_with_postcount
+ if taxonomy.has_hierarchy and taxonomy.overview_page_hierarchy_variable_name:
+ hier_items = [
+ (node.name, node.classification_name, node.classification_path,
+ self.site.link(taxonomy.classification_name, node.classification_name, lang),
+ node.indent_levels, node.indent_change_before,
+ node.indent_change_after)
+ for node in clipped_flat_hierarchy
+ ]
+ hier_items_with_postcount = [
+ (node.name, node.classification_name, node.classification_path,
+ self.site.link(taxonomy.classification_name, node.classification_name, lang),
+ node.indent_levels, node.indent_change_before,
+ node.indent_change_after,
+ len(node.children),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang)))
+ for node in clipped_flat_hierarchy
+ ]
+ context[taxonomy.overview_page_hierarchy_variable_name] = hier_items
+ context[taxonomy.overview_page_hierarchy_variable_name + '_with_postcount'] = hier_items_with_postcount
+ return context, kw
+
+ def _render_classification_overview(self, classification_name, template, lang, context, kw):
+ # Prepare rendering
+ context["permalink"] = self.site.link("{}_index".format(classification_name), None, lang)
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "tags_page"]
+ output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(classification_name), None, lang))
+ blinker.signal('generate_classification_overview').send({
+ 'site': self.site,
+ 'classification_name': classification_name,
+ 'lang': lang,
+ 'context': context,
+ 'kw': kw,
+ 'output_name': output_name,
+ })
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template,
+ kw['filters'],
+ context,
+ )
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:page')]
+ task['basename'] = str(self.name)
+ yield task
+
+ def _generate_classification_overview(self, taxonomy, lang):
+ """Create a global "all your tags/categories" page for a given language."""
+ context, kw = self._generate_classification_overview_kw_context(taxonomy, lang)
+ for task in self._render_classification_overview(taxonomy.classification_name, taxonomy.template_for_classification_overview, lang, context, kw):
+ yield task
+
+ def _generate_tag_and_category_overview(self, tag_taxonomy, category_taxonomy, lang):
+ """Create a global "all your tags/categories" page for a given language."""
+ # Create individual contexts and kw dicts
+ tag_context, tag_kw = self._generate_classification_overview_kw_context(tag_taxonomy, lang)
+ cat_context, cat_kw = self._generate_classification_overview_kw_context(category_taxonomy, lang)
+
+ # Combine resp. select dicts
+ if tag_context['items'] and cat_context['cat_items']:
+ # Combine contexts. We must merge the tag context into the category context
+ # so that tag_context['items'] makes it into the result.
+ context = cat_context
+ context.update(tag_context)
+ kw = cat_kw
+ kw.update(tag_kw)
+
+ # Update title
+ title = self.site.MESSAGES[lang]["Tags and Categories"]
+ context['title'] = title
+ context['description'] = title
+ kw['title'] = title
+ kw['description'] = title
+ elif cat_context['cat_items']:
+ # Use category overview page
+ context = cat_context
+ kw = cat_kw
+ else:
+ # Use tag overview page
+ context = tag_context
+ kw = tag_kw
+
+ # Render result
+ for task in self._render_classification_overview('tag', tag_taxonomy.template_for_classification_overview, lang, context, kw):
+ yield task
+
+ def _generate_classification_page_as_rss(self, taxonomy, classification, filtered_posts, title, description, kw, lang):
+ """Create a RSS feed for a single classification in a given language."""
+ kind = taxonomy.classification_name
+ # Render RSS
+ output_name = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind + "_rss", classification, lang)))
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", classification, lang).lstrip('/'))
+ deps = []
+ deps_uptodate = []
+ for post in filtered_posts:
+ deps += post.deps(lang)
+ deps_uptodate += post.deps_uptodate(lang)
+ blog_title = kw["blog_title"](lang)
+ task = {
+ 'basename': str(self.name),
+ 'name': output_name,
+ 'file_dep': deps,
+ 'targets': [output_name],
+ 'actions': [(utils.generic_rss_renderer,
+ (lang, "{0} ({1})".format(blog_title, title) if blog_title != title else blog_title,
+ kw["site_url"], description, filtered_posts,
+ output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
+ feed_url, _enclosure, kw["feed_links_append_query"]))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:rss')] + deps_uptodate,
+ 'task_dep': ['render_posts'],
+ }
+ return utils.apply_filters(task, kw['filters'])
+
+ def _generate_classification_page_as_index(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Render an index page collection using only this classification's posts."""
+ kind = taxonomy.classification_name
+
+ def page_link(i, displayed_i, num_pages, force_addition, extension=None):
+ return self.site.link(kind, classification, lang, alternative_path=force_addition, page=i)
+
+ def page_path(i, displayed_i, num_pages, force_addition, extension=None):
+ return self.site.path(kind, classification, lang, alternative_path=force_addition, page=i)
+
+ context = copy(context)
+ context["kind"] = kind
+ if "pagekind" not in context:
+ context["pagekind"] = ["index", "tag_page"]
+ template_name = taxonomy.template_for_single_list
+
+ yield self.site.generic_index_renderer(lang, filtered_posts, context['title'], template_name, context, kw, str(self.name), page_link, page_path)
+
+ def _generate_classification_page_as_atom(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Generate atom feeds for classification lists."""
+ kind = taxonomy.classification_name
+
+ context = copy(context)
+ context["kind"] = kind
+
+ yield self.site.generic_atom_renderer(lang, filtered_posts, context, kw, str(self.name), classification, kind)
+
+ def _generate_classification_page_as_list(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Render a single flat link list with this classification's posts."""
+ kind = taxonomy.classification_name
+ template_name = taxonomy.template_for_single_list
+ output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang))
+ context["lang"] = lang
+ # list.tmpl expects a different format than list_post.tmpl (Issue #2701)
+ if template_name == 'list.tmpl':
+ context["items"] = [(post.title(lang), post.permalink(lang), None) for post in filtered_posts]
+ else:
+ context["posts"] = filtered_posts
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "tag_page"]
+ task = self.site.generic_post_list_renderer(lang, filtered_posts, output_name, template_name, kw['filters'], context)
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:list')]
+ task['basename'] = str(self.name)
+ yield task
+
+ def _filter_list(self, post_list, lang):
+ """Return only the posts which should be shown for this language."""
+ if self.site.config["SHOW_UNTRANSLATED_POSTS"]:
+ return post_list
+ else:
+ return [x for x in post_list if x.is_translation_available(lang)]
+
+ def _generate_subclassification_page(self, taxonomy, node, context, kw, lang):
+ """Render a list of subclassifications."""
+ def get_subnode_data(subnode):
+ return [
+ taxonomy.get_classification_friendly_name(subnode.classification_name, lang, only_last_component=True),
+ self.site.link(taxonomy.classification_name, subnode.classification_name, lang),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][subnode.classification_name], lang))
+ ]
+
+ items = [get_subnode_data(subnode) for subnode in node.children]
+ context = copy(context)
+ context["lang"] = lang
+ context["permalink"] = self.site.link(taxonomy.classification_name, node.classification_name, lang)
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "archive_page"]
+ context["items"] = items
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ os.path.join(kw['output_folder'], self.site.path(taxonomy.classification_name, node.classification_name, lang)),
+ taxonomy.subcategories_list_template,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: kw, 2: items}
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(task_cfg, 'nikola.plugins.task.taxonomy')]
+ task['basename'] = self.name
+ return task
+
+ def _generate_classification_page(self, taxonomy, classification, filtered_posts, generate_list, generate_rss, generate_atom, lang, post_lists_per_lang, classification_set_per_lang=None):
+ """Render index or post list and associated feeds per classification."""
+ # Should we create this list?
+ if not any((generate_list, generate_rss, generate_atom)):
+ return
+ # Get data
+ node = None
+ if taxonomy.has_hierarchy:
+ node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang].get(classification)
+ context, kw = taxonomy.provide_context_and_uptodate(classification, lang, node)
+ kw = copy(kw)
+ kw["messages"] = self.site.MESSAGES
+ kw["translations"] = self.site.config['TRANSLATIONS']
+ kw["filters"] = self.site.config['FILTERS']
+ kw["site_url"] = self.site.config['SITE_URL']
+ kw["blog_title"] = self.site.config['BLOG_TITLE']
+ kw["generate_rss"] = self.site.config['GENERATE_RSS']
+ kw["generate_atom"] = self.site.config['GENERATE_ATOM']
+ kw["feed_teasers"] = self.site.config["FEED_TEASERS"]
+ kw["feed_plain"] = self.site.config["FEED_PLAIN"]
+ kw["feed_links_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"]
+ kw["feed_length"] = self.site.config['FEED_LENGTH']
+ kw["output_folder"] = self.site.config['OUTPUT_FOLDER']
+ kw["pretty_urls"] = self.site.config['PRETTY_URLS']
+ kw["strip_indexes"] = self.site.config['STRIP_INDEXES']
+ kw["index_file"] = self.site.config['INDEX_FILE']
+ context = copy(context)
+ context["permalink"] = self.site.link(taxonomy.classification_name, classification, lang)
+ context["kind"] = taxonomy.classification_name
+ # Get links to other language versions of this classification
+ if classification_set_per_lang is not None:
+ other_lang_links = taxonomy.get_other_language_variants(classification, lang, classification_set_per_lang)
+ # Collect by language
+ links_per_lang = defaultdict(list)
+ for other_lang, link in other_lang_links:
+ # Make sure we ignore the current language (in case the
+ # plugin accidentally returns links for it as well)
+ if other_lang != lang:
+ links_per_lang[other_lang].append(link)
+ # Sort first by language, then by classification
+ sorted_links = []
+ sorted_links_all = []
+ for other_lang in sorted(list(links_per_lang.keys()) + [lang]):
+ if other_lang == lang:
+ sorted_links_all.append((lang, classification, taxonomy.get_classification_friendly_name(classification, lang)))
+ else:
+ links = hierarchy_utils.sort_classifications(taxonomy, links_per_lang[other_lang], other_lang)
+ links = [(other_lang, other_classification,
+ taxonomy.get_classification_friendly_name(other_classification, other_lang))
+ for other_classification in links if post_lists_per_lang[other_lang].get(other_classification, ('', False, False))[1]]
+ sorted_links.extend(links)
+ sorted_links_all.extend(links)
+ # Store result in context and kw
+ context['has_other_languages'] = True
+ context['other_languages'] = sorted_links
+ context['all_languages'] = sorted_links_all
+ kw['other_languages'] = sorted_links
+ kw['all_languages'] = sorted_links_all
+ else:
+ context['has_other_languages'] = False
+ # Allow other plugins to modify the result
+ blinker.signal('generate_classification_page').send({
+ 'site': self.site,
+ 'taxonomy': taxonomy,
+ 'classification': classification,
+ 'lang': lang,
+ 'posts': filtered_posts,
+ 'context': context,
+ 'kw': kw,
+ })
+ # Decide what to do
+ if taxonomy.has_hierarchy and taxonomy.show_list_as_subcategories_list:
+ # Determine whether there are subcategories
+ node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang][classification]
+ # Are there subclassifications?
+ if len(node.children) > 0:
+ # Yes: create list with subclassifications instead of list of posts
+ if generate_list:
+ yield self._generate_subclassification_page(taxonomy, node, context, kw, lang)
+ return
+ # Generate RSS feed
+ if generate_rss and kw["generate_rss"] and not taxonomy.always_disable_rss:
+ yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], context.get("description"), kw, lang)
+
+ # Generate Atom feed
+ if generate_atom and kw["generate_atom"] and not taxonomy.always_disable_atom:
+ yield self._generate_classification_page_as_atom(taxonomy, classification, filtered_posts, context, kw, lang)
+
+ # Render HTML
+ if generate_list and taxonomy.show_list_as_index:
+ yield self._generate_classification_page_as_index(taxonomy, classification, filtered_posts, context, kw, lang)
+ elif generate_list:
+ yield self._generate_classification_page_as_list(taxonomy, classification, filtered_posts, context, kw, lang)
+
+ def gen_tasks(self):
+ """Render the tag pages and feeds."""
+ self.site.scan_posts()
+ yield self.group_task()
+
+ # Cache classification sets per language for taxonomies where
+ # add_other_languages_variable is True.
+ classification_set_per_lang = {}
+ for taxonomy in self.site.taxonomy_plugins.values():
+ if taxonomy.add_other_languages_variable:
+ lookup = self.site.posts_per_classification[taxonomy.classification_name]
+ cspl = {lang: set(lookup[lang].keys()) for lang in lookup}
+ classification_set_per_lang[taxonomy.classification_name] = cspl
+
+ # Collect post lists for classification pages and determine whether
+ # they should be generated.
+ post_lists_per_lang = {}
+ for taxonomy in self.site.taxonomy_plugins.values():
+ plpl = {}
+ for lang in self.site.config["TRANSLATIONS"]:
+ result = {}
+ for classification, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items():
+ # Filter list
+ filtered_posts = self._filter_list(posts, lang)
+ if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications:
+ generate_list = generate_rss = generate_atom = False
+ else:
+ # Should we create this list?
+ generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang)
+ generate_rss = taxonomy.should_generate_rss_for_classification_page(classification, filtered_posts, lang)
+ generate_atom = taxonomy.should_generate_atom_for_classification_page(classification, filtered_posts, lang)
+ result[classification] = (filtered_posts, generate_list, generate_rss, generate_atom)
+ plpl[lang] = result
+ post_lists_per_lang[taxonomy.classification_name] = plpl
+
+ # Now generate pages
+ for lang in self.site.config["TRANSLATIONS"]:
+ # To support that tag and category classifications share the same overview,
+ # we explicitly detect this case:
+ ignore_plugins_for_overview = set()
+ if 'tag' in self.site.taxonomy_plugins and 'category' in self.site.taxonomy_plugins and self.site.link("tag_index", None, lang) == self.site.link("category_index", None, lang):
+ # Block both plugins from creating overviews
+ ignore_plugins_for_overview.add(self.site.taxonomy_plugins['tag'])
+ ignore_plugins_for_overview.add(self.site.taxonomy_plugins['category'])
+ for taxonomy in self.site.taxonomy_plugins.values():
+ if not taxonomy.is_enabled(lang):
+ continue
+ # Generate list of classifications (i.e. classification overview)
+ if taxonomy not in ignore_plugins_for_overview:
+ if taxonomy.template_for_classification_overview is not None:
+ for task in self._generate_classification_overview(taxonomy, lang):
+ yield task
+
+ # Process classifications
+ for classification, (filtered_posts, generate_list, generate_rss, generate_atom) in post_lists_per_lang[taxonomy.classification_name][lang].items():
+ for task in self._generate_classification_page(taxonomy, classification, filtered_posts,
+ generate_list, generate_rss, generate_atom, lang,
+ post_lists_per_lang[taxonomy.classification_name],
+ classification_set_per_lang.get(taxonomy.classification_name)):
+ yield task
+ # In case we are ignoring plugins for overview, we must have a collision for
+ # tags and categories. Handle this special case with extra code.
+ if ignore_plugins_for_overview:
+ for task in self._generate_tag_and_category_overview(self.site.taxonomy_plugins['tag'], self.site.taxonomy_plugins['category'], lang):
+ yield task