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