diff options
Diffstat (limited to 'nikola/plugins/task')
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 |
