Instantly share code, notes, and snippets.
Last active
September 21, 2015 20:28
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save felixfontein/38468f5d34cedd5148e6 to your computer and use it in GitHub Desktop.
Extended tags plugins for Nikola.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Core] | |
Name = extended_tags | |
Module = extended_tags | |
[Documentation] | |
Author = Felix Fontein | |
Version = 1.0 | |
Website = https://spielwiese.fontein.de/ | |
Description = Allows more complex kind of tags. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Provides second plugin for extended tag support for Nikola.""" | |
from __future__ import unicode_literals, print_function, absolute_import | |
from nikola.plugin_categories import Task | |
from nikola import utils | |
import json | |
import os | |
try: | |
from urlparse import urljoin | |
except ImportError: | |
from urllib.parse import urljoin # NOQA | |
__all__ = ('ExtendedTags', ) | |
_LOGGER = utils.get_logger('extended_tags', utils.STDERR_HANDLER) | |
def _clone_treenode(treenode, parent=None, acceptor=lambda x: True): | |
# Standard TreeNode stuff | |
node_clone = utils.TreeNode(treenode.name, parent) | |
node_clone.children = [_clone_treenode(node, parent=node_clone, acceptor=acceptor) for node in treenode.children if acceptor(node)] | |
node_clone.indent_levels = treenode.indent_levels | |
node_clone.indent_change_before = treenode.indent_change_before | |
node_clone.indent_change_after = treenode.indent_change_after | |
# Stuff added by extended_tags_preproces plugin | |
node_clone.tag_path = treenode.tag_path | |
node_clone.tag_name = treenode.tag_name | |
return node_clone | |
class ExtendedTags(Task): | |
"""Render the tag/category pages and feeds.""" | |
name = "render_extended_tags" | |
def gen_target_regex(self): | |
"""Return a regex which matches all targets generated by this task, and hopefully nothing else.""" | |
import re | |
regexes = set() | |
for lang in self.site.config['TRANSLATIONS']: | |
for type, type_config in self.site.extended_tags_setup.items(): | |
path = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{0}_index'.format(type), None, lang)) | |
path = path[:-len(self.site.config['INDEX_FILE'])] | |
regexes.add(re.escape(path) + ".*") | |
return '(?:' + '|'.join(sorted(list(regexes))) + ')' if len(regexes) > 0 else '' | |
def _get_title(self, tag, type, type_config): | |
return self.site.extended_tags_parse_name(tag, type_config)[-1] | |
def _tag_rss(self, tag, lang, posts, kw, type, type_config): | |
"""Create a RSS feed for a single tag in a given language.""" | |
# Render RSS | |
output_name = os.path.normpath(os.path.join(kw['output_folder'], self.site.path("{0}_rss".format(type), tag, lang))) | |
feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("{0}_rss".format(type), tag, lang).lstrip('/')) | |
deps = [] | |
deps_uptodate = [] | |
post_list = list(posts) | |
for post in post_list: | |
deps += post.deps(lang) | |
deps_uptodate += post.deps_uptodate(lang) | |
task = { | |
'basename': str(self.name), | |
'name': output_name, | |
'file_dep': deps, | |
'targets': [output_name], | |
'actions': [(utils.generic_rss_renderer, | |
(lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, type, type_config)), | |
kw["site_url"], None, post_list, | |
output_name, kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], | |
feed_url, None, kw["rss_link_append_query"]))], | |
'clean': True, | |
'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate, | |
'task_dep': ['render_posts'], | |
} | |
return utils.apply_filters(task, kw['filters']) | |
def _get_indexes_title(self, tag, type, type_config, lang, messages): | |
titles = type_config['titles'] | |
return titles[lang][tag] if lang in titles and tag in titles[lang] else messages[lang]["Posts about %s"] % tag | |
def _get_description(self, tag, type, type_config, lang): | |
descriptions = type_config['descriptions'] | |
return descriptions[lang][tag] if lang in descriptions and tag in descriptions[lang] else None | |
def _get_subtags(self, tag, lang, type, type_config): | |
hierarchy = (type_config['hierarchy_lookup'][lang] if type_config['only_tags_from_this_language'] else type_config['hierarchy_lookup_all']) | |
node = hierarchy[tag] | |
children = node.children | |
if type_config['omit_empty_tags']: | |
posts = (type_config['hierarchy_posts'][lang] if type_config['only_tags_from_this_language'] else type_config['hierarchy_posts_all']) | |
children = [child for child in children if len([post for post in posts.get(child.tag_name, []) if self.site.config['SHOW_UNTRANSLATED_POSTS'] or post.is_translation_available(lang)]) > 0] | |
return [(child.name, self.site.link(type, child.tag_name), child.tag_name, child.tag_path) for child in children] | |
def _tag_page_as_index(self, tag, lang, post_list, kw, type, type_config): | |
"""Render a sort of index page collection using only this tag's posts.""" | |
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(type + 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(type + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension) | |
context_source = {} | |
title = self._get_title(tag, type, type_config) | |
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("{0}_rss".format(type), tag, lang))) | |
context_source['rss_link'] = rss_link | |
context_source["{0}".format(type)] = tag | |
context_source["{0}_path".format(type)] = self.site.extended_tags_parse_name(tag, type_config) | |
tag_path = self.site.extended_tags_parse_name(tag, type_config) | |
context_source["tag_name"] = tag | |
context_source["tag_path"] = tag_path | |
partial_tag_paths = [tag_path[:i + 1] for i in range(len(tag_path))] | |
context_source["tag_partial_paths"] = partial_tag_paths | |
context_source["tag_partial_names"] = [self.site.extended_tags_combine_path(partial_tag_path, type_config) for partial_tag_path in partial_tag_paths] | |
context_source["tag_type"] = type | |
context_source["tag"] = title | |
indexes_title = self._get_indexes_title(title, type, type_config, lang, kw["messages"]) | |
context_source["description"] = self._get_description(tag, type, type_config, lang) | |
context_source["sub{0}".format(type_config['pluralname'])] = self._get_subtags(tag, lang, type, type_config) | |
context_source["subtags"] = context_source["sub{0}".format(type_config['pluralname'])] | |
context_source["pagekind"] = ["index", "tag_page"] | |
template_name = type_config['tag_page_index_template'] | |
yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path) | |
def atom_feed_list(self, kind, tag, lang, post_list, context, kw): | |
"""Generate atom feeds for tag lists.""" | |
context['feedlink'] = self.site.abs_link(self.site.path('{0}_atom'.format(kind), tag, lang)) | |
feed_path = os.path.join(kw['output_folder'], self.site.path('{0}_atom'.format(kind), tag, lang)) | |
task = { | |
'basename': str(self.name), | |
'name': feed_path, | |
'targets': [feed_path], | |
'actions': [(self.site.atom_feed_renderer, (lang, post_list, feed_path, kw['filters'], context))], | |
'clean': True, | |
'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:atom')], | |
'task_dep': ['render_posts'], | |
} | |
return task | |
def _tag_page_as_list(self, tag, lang, post_list, kw, type, type_config): | |
"""Render a single flat link list with this tag's posts.""" | |
template_name = type_config['tag_page_list_template'] | |
kw = kw.copy() | |
output_name = os.path.join(kw['output_folder'], self.site.path(type, tag, lang)) | |
context = {} | |
context["lang"] = lang | |
title = self._get_title(tag, type, type_config) | |
context["{0}".format(type)] = tag | |
context["{0}_path".format(type)] = self.site.extended_tags_parse_name(tag, type_config) | |
tag_path = self.site.extended_tags_parse_name(tag, type_config) | |
context["tag_name"] = tag | |
context["tag_path"] = tag_path | |
partial_tag_paths = [tag_path[:i + 1] for i in range(len(tag_path))] | |
context["tag_partial_paths"] = partial_tag_paths | |
context["tag_partial_names"] = [self.site.extended_tags_combine_path(partial_tag_path, type_config) for partial_tag_path in partial_tag_paths] | |
context["tag_type"] = type | |
context["tag"] = title | |
context["title"] = self._get_indexes_title(title, type, type_config, lang, kw["messages"]) | |
context["posts"] = post_list | |
context["permalink"] = self.site.link(type, tag, lang) | |
context["kind"] = type | |
context["description"] = self._get_description(tag, type, type_config, lang) | |
context["sub{0}".format(type_config['pluralname'])] = self._get_subtags(tag, lang, type, type_config) | |
context["subtags"] = context["sub{0}".format(type_config['pluralname'])] | |
context["pagekind"] = ["list", "tag_page"] | |
task = self.site.generic_post_list_renderer( | |
lang, | |
post_list, | |
output_name, | |
template_name, | |
kw['filters'], | |
context.copy(), | |
) | |
task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')] | |
task['basename'] = str(self.name) | |
yield task | |
if self.site.config['GENERATE_ATOM']: | |
yield self.atom_feed_list(type, tag, lang, post_list, context, kw) | |
def _create_tags_page(self, master_kw, lang, types): | |
"""Create a global "all your tags/categories" page for each language.""" | |
template_name = "tags.tmpl" | |
kw = master_kw.copy() | |
context = {} | |
context['items_dict'] = {} | |
context['hierarchy_dict'] = {} | |
types_ = [] | |
for type in types: | |
type_config = self.site.extended_tags_setup[type] | |
acceptor = lambda node: node.tag_name not in type_config['hidden'] and len(type_config['hierarchy_posts'][lang][node.tag_name]) >= type_config['list_minimum_posts'] and (not type_config['omit_empty_tags'] or len(type_config['hierarchy_posts'][lang][node.tag_name]) > 0) | |
tags_tree = [_clone_treenode(node, parent=None, acceptor=acceptor) for node in type_config['hierarchy_tree'][lang] if acceptor(node)] | |
if tags_tree: | |
types_.append(type) | |
tags = utils.flatten_tree_structure(tags_tree) | |
kw[type_config['pluralname']] = [node.tag_name for node in tags] | |
context['{0}_items'.format(type)] = [(node.tag_name, self.site.link(type, node.tag_name, lang)) for node in tags] | |
context['{0}_hierarchy'.format(type)] = [(node.name, node.tag_name, node.tag_path, | |
self.site.link(type, node.tag_name, lang), | |
node.indent_levels, node.indent_change_before, | |
node.indent_change_after, len(node.children)) | |
for node in tags] | |
else: | |
context['{0}_items'.format(type)] = None | |
context['{0}_hierarchy'.format(type)] = None | |
context['items_dict'][type] = context['{0}_items'.format(type)] | |
context['hierarchy_dict'][type] = context['{0}_hierarchy'.format(type)] | |
# For backwards compatibility | |
if type == 'tag': | |
context['items'] = context['{0}_items'.format(type)] | |
elif type == 'category': | |
context['cat_items'] = context['{0}_items'.format(type)] | |
context['cat_hierarchy'] = context['{0}_hierarchy'.format(type)] | |
if types_ == ["category", "tag"]: | |
context["title"] = kw["messages"][lang]["Tags and Categories"] | |
elif types_ == ["category"]: | |
context["title"] = kw["messages"][lang]["Categories"] | |
elif types_ == ["type"]: | |
context["title"] = kw["messages"][lang]["Tags"] | |
else: | |
# FIXME! | |
if 'category' in types_ and len(types_) > 1: | |
context["title"] = kw["messages"][lang]["Tags and Categories"] | |
else: | |
context["title"] = kw["messages"][lang]["Tags"] | |
output_name = os.path.join(kw['output_folder'], self.site.path('{0}_index'.format(types[0]), None, lang)) | |
context['tag_types'] = types | |
context['real_tag_types'] = types_ | |
context["permalink"] = self.site.link('{0}_index'.format(types[0]), None, lang) | |
context["description"] = context["title"] | |
context["pagekind"] = ["list", "tags_page"] | |
task = self.site.generic_post_list_renderer( | |
lang, | |
[], | |
output_name, | |
template_name, | |
kw['filters'], | |
context, | |
) | |
task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:page')] | |
task['basename'] = str(self.name) | |
yield task | |
def gen_tasks(self): | |
"""Render the tag pages and feeds.""" | |
master_kw = { | |
"translations": self.site.config["TRANSLATIONS"], | |
"blog_title": self.site.config["BLOG_TITLE"], | |
"site_url": self.site.config["SITE_URL"], | |
"base_url": self.site.config["BASE_URL"], | |
"messages": self.site.MESSAGES, | |
"output_folder": self.site.config['OUTPUT_FOLDER'], | |
"filters": self.site.config['FILTERS'], | |
"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'], | |
"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'], | |
} | |
def create_kw_copy(types, lang): | |
kw = master_kw.copy() | |
for type in types: | |
idx = '{0}_config'.format(type) | |
kw[idx] = {k: v for k, v in self.site.extended_tags_setup[type].items() if k not in {'hierarchy', 'hierarchy_all', 'hierarchy_lookup', 'hierarchy_lookup_all', 'hierarchy_tree', 'hierarchy_tree_all', 'hierarchy_posts', 'hierarchy_posts_all'}} | |
kw[idx]['path'] = kw[idx]['path'][lang] | |
kw[idx]['descriptions'] = kw[idx]['descriptions'].get('lang', {}) | |
kw[idx]['titles'] = kw[idx]['titles'].get('lang', {}) | |
return kw | |
self.site.scan_posts() # FIXME: not needed with earlytasks_impl branch | |
yield self.group_task() | |
for lang in master_kw['translations']: | |
# Find out which types belong to the same path | |
tag_paths = {} | |
for type, type_config in self.site.extended_tags_setup.items(): | |
index = tuple(type_config['path'][lang].split('/')) | |
if index not in tag_paths: | |
tag_paths[index] = set() | |
tag_paths[index].add(type) | |
# Create tag pages | |
for path, types in tag_paths.items(): | |
# Get sorted list | |
types = list(types) | |
types.sort() | |
# Create kw for this tag type | |
kw = create_kw_copy(types, lang) | |
kw['tag_types'] = types | |
yield self._create_tags_page(kw, lang, types) | |
# Go through all tag types | |
for type, type_config in self.site.extended_tags_setup.items(): | |
# If there's nothing to do, ignore type | |
if not type_config['hierarchy_posts_all']: | |
continue | |
for lang in kw["translations"]: | |
# Create kw for this tag type | |
kw = create_kw_copy((type, ), lang) | |
kw['tag_type'] = type | |
# Render tag lists | |
def render_lists(tag, posts, type, type_config, kw, lang): | |
"""Render tag pages as RSS files and lists/indexes.""" | |
post_list = list(posts) | |
if kw["show_untranslated_posts"]: | |
filtered_posts = post_list | |
else: | |
filtered_posts = [x for x in post_list if x.is_translation_available(lang)] | |
if type_config['omit_empty_tags'] and len(filtered_posts) == 0: | |
return | |
if kw["generate_rss"]: | |
yield self._tag_rss(tag, lang, filtered_posts, kw, type, type_config) | |
# Render HTML | |
if type_config['pages_are_indices']: | |
yield self._tag_page_as_index(tag, lang, filtered_posts, kw, type, type_config) | |
else: | |
yield self._tag_page_as_list(tag, lang, filtered_posts, kw, type, type_config) | |
for tag, posts in (type_config['hierarchy_posts'][lang] if type_config['only_tags_from_this_language'] else type_config['hierarchy_posts_all']).items(): | |
for task in render_lists(tag, posts, type, type_config, kw, lang): | |
yield task | |
# Write tag cloud JSON file | |
if type_config['tag_cloud_path']: | |
# Create kw for this tag type | |
kw = create_kw_copy((type, ), self.site.default_lang) | |
kw['tag_type'] = type | |
tag_cloud_data = {} | |
for tag, posts in (type_config['hierarchy_posts'][lang] if type_config['only_tags_from_this_language'] else type_config['hierarchy_posts_all']).items(): | |
if tag in type_config['hidden']: | |
continue | |
tag_posts = dict(posts=[{'title': post.meta[post.default_lang]['title'], | |
'date': post.date.strftime('%m/%d/%Y'), | |
'isodate': post.date.isoformat(), | |
'url': post.permalink(post.default_lang)} | |
for post in reversed(sorted(self.site.timeline, key=lambda post: post.date)) | |
if tag in post.alltags]) # FIXME: use real type, not always 'tag'! | |
tag_cloud_data[tag] = [len(posts), self.site.link(type, tag, self.site.default_lang), tag_posts] | |
output_name = os.path.join(kw['output_folder'], *type_config['tag_cloud_path']) | |
task = { | |
'basename': str(self.name), | |
'name': str(output_name) | |
} | |
def write_tag_data(data): | |
"""Write tag data into JSON file, for use in tag clouds.""" | |
utils.makedirs(os.path.dirname(output_name)) | |
with open(output_name, 'w+') as fd: | |
json.dump(data, fd) | |
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']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Core] | |
Name = extended_tags_preprocess | |
Module = extended_tags_preprocess | |
[Documentation] | |
Author = Felix Fontein | |
Version = 1.0 | |
Website = https://spielwiese.fontein.de/ | |
Description = Allows more complex kind of tags. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Provides first plugin for extended tag support for Nikola.""" | |
from __future__ import unicode_literals, print_function, absolute_import | |
from nikola.plugin_categories import SignalHandler | |
from nikola import utils | |
import blinker | |
import copy | |
import natsort | |
import sys | |
import traceback | |
__all__ = ('ExtendedTagsPreprocess', ) | |
_LOGGER = utils.get_logger('extended_tags', utils.STDERR_HANDLER) | |
_TYPE_CONFIG_MUST_BE_SPECIFIED = ['pluralname', 'sources', 'path'] | |
_TRANSLATABLE_TYPE_CONFIG = ['path'] | |
_DEFAULT_TYPE_CONFIG = { | |
# 'pluralname': <str>, | |
# 'sources': <list>, | |
# 'path': <str>, # translatable | |
'prefix': '', | |
'pages_are_indices': False, | |
'slug_paths': True, | |
'descriptions': { | |
}, | |
'titles': { | |
}, | |
'ignore': [], | |
'hidden': [], | |
'list_minimum_posts': 1, | |
'has_hierarchy': False, | |
'output_flat_hierarchy': False, | |
'tag_page_list_template': 'tag.tmpl', | |
'tag_page_index_template': 'tagindex.tmpl', | |
'tag_cloud_path': None, # None for don't emit, otherwise string specifying file name (dumped in JSON format) | |
'omit_empty_tags': False, | |
'only_tags_from_this_language': False, | |
} | |
_SOURCE_CONFIG_MUST_BE_SPECIFIED = ['identifier'] | |
_DEFAULT_SOURCE_CONFIG = { | |
# 'identifier': <str>, | |
'can_specify_multiple': True, | |
} | |
class ExtendedTagsPreprocess(SignalHandler): | |
"""Does preprocessing (config parsing, tags reading) for extended tags support.""" | |
name = "preprocess_extended_tags" | |
def _parse_config(self, config): | |
"""Parse config, or sets up default config using legacy configs.""" | |
# Try to get config | |
self._setup = config.get('EXTENDED_TAGS_CONFIG', None) | |
if self._setup is None: | |
# If no config is given, use a default config | |
self._setup = { | |
'tag': { | |
'pluralname': 'tags', | |
'sources': [{'identifier': 'tags', 'can_specify_multiple': True}], | |
'path': self.site.config.get('TAG_PATH', 'categories'), | |
'prefix': '', | |
'pages_are_indices': self.site.config.get('TAG_PAGES_ARE_INDEXES', False), | |
'slug_paths': self.site.config.get('SLUG_TAG_PATH', True), | |
'descriptions': self.site.config.get('TAG_PAGES_DESCRIPTIONS', {}), # translatable | |
'titles': self.site.config.get('TAG_PAGES_TITLES', {}), # translatable | |
'ignore': ['draft', 'retired', 'private'], | |
'hidden': self.site.config.get('HIDDEN_TAGS', ['mathjax']), | |
'list_minimum_posts': self.site.config.get('TAGLIST_MINIMUM_POSTS', 1), | |
'has_hierarchy': False, | |
'output_flat_hierarchy': False, | |
'tag_cloud_path': 'assets/js/tag_cloud_data.json' if self.site.config.get('WRITE_TAG_CLOUD', True) else None, | |
'omit_empty_tags': False, | |
'only_tags_from_this_language': False, | |
}, | |
'category': { | |
'pluralname': 'categories', | |
'sources': [{'identifier': 'category', 'can_specify_multiple': False}], | |
'path': self.site.config.get('CATEGORY_PATH', 'categories'), | |
'prefix': self.site.config.get('CATEGORY_PREFIX', 'cat_'), | |
'pages_are_indices': self.site.config.get('CATEGORY_PAGES_ARE_INDEXES', False), | |
'slug_paths': self.site.config.get('SLUG_TAG_PATH', True), | |
'descriptions': self.site.config.get('CATEGORY_PAGES_DESCRIPTIONS', {}), # translatable | |
'titles': self.site.config.get('CATEGORY_PAGES_TITLES', {}), # translatable | |
'ignore': [], | |
'hidden': self.site.config.get('HIDDEN_CATEGORIES', []), | |
'list_minimum_posts': 1, | |
'has_hierarchy': self.site.config.get('CATEGORY_ALLOW_HIERARCHIES', False), | |
'output_flat_hierarchy': self.site.config.get('CATEGORY_OUTPUT_FLAT_HIERARCHY', False), | |
'tag_cloud_path': None, | |
'omit_empty_tags': False, | |
'only_tags_from_this_language': False, | |
} | |
} | |
# Sanitize config | |
for type in self._setup: | |
for config in _TYPE_CONFIG_MUST_BE_SPECIFIED: | |
if config not in self._setup[type]: | |
raise Exception("Extended tag configuration for type '{0}' must contain '{1}'!".format(type, config)) | |
for config in _DEFAULT_TYPE_CONFIG: | |
if config not in self._setup[type]: | |
self._setup[type][config] = copy.copy(_DEFAULT_TYPE_CONFIG[config]) | |
for config in self._setup[type]: | |
if config not in _TYPE_CONFIG_MUST_BE_SPECIFIED and config not in _DEFAULT_TYPE_CONFIG: | |
_LOGGER.warn("Extended tag configuration for type '{0}' contains unknown setting '{1}'!".format(type, config)) | |
for config in _TRANSLATABLE_TYPE_CONFIG: | |
if not isinstance(self._setup[type][config], utils.TranslatableSetting): | |
self._setup[type][config] = utils.TranslatableSetting(config, self._setup[type][config], self.site.config['TRANSLATIONS']) | |
for source in self._setup[type]['sources']: | |
for config in _SOURCE_CONFIG_MUST_BE_SPECIFIED: | |
if config not in source: | |
raise Exception("Source '{0}' for extended tag type '{1}' must contain '{2}'!".format(source, type, config)) | |
for config in _DEFAULT_SOURCE_CONFIG: | |
if config not in source: | |
source[config] = copy.copy(_DEFAULT_SOURCE_CONFIG[config]) | |
for config in source: | |
if config not in _SOURCE_CONFIG_MUST_BE_SPECIFIED and config not in _DEFAULT_SOURCE_CONFIG: | |
_LOGGER.warn("Source '{0}' for extended tag type '{1}' contains unknown setting '{2}'!".format(source, type, config)) | |
# Finally, optimize config | |
for type in self._setup: | |
self._setup[type]['ignore'] = set(self._setup[type]['ignore']) | |
self._setup[type]['hidden'] = set(self._setup[type]['hidden']) | |
self._setup[type]['hierarchy_tree'] = {lang: {} for lang in self.site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_posts'] = {lang: {} for lang in self.site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_tree_all'] = {} | |
self._setup[type]['hierarchy_posts_all'] = {} | |
if self._setup[type]['tag_cloud_path']: | |
self._setup[type]['tag_cloud_path'] = [elt for elt in self._setup[type]['tag_cloud_path'].split('/') if elt] | |
else: | |
self._setup[type]['tag_cloud_path'] = None | |
@staticmethod | |
def _parse_name(name, type_config): | |
"""Translate a name to a path.""" | |
if type_config['has_hierarchy']: | |
return utils.parse_escaped_hierarchical_category_name(name) | |
else: | |
return [name] if name else [] | |
@staticmethod | |
def _combine_path(path, type_config): | |
"""Translate a path to a name.""" | |
if type_config['has_hierarchy']: | |
return utils.join_hierarchical_category_path(path) | |
else: | |
return ''.join(path) | |
def _add_to_hierarchy(self, post, name, path, type_config, lang): | |
"""Add a post to a hierarchy.""" | |
def add(current_subtree, hierarchy_posts): | |
current_path = [] | |
for current in path: | |
current_path.append(current) | |
# Add subtree | |
if current not in current_subtree: | |
current_subtree[current] = {} | |
current_subtree = current_subtree[current] | |
# Add post | |
current_name = self._combine_path(current_path, type_config) | |
if current_name not in hierarchy_posts: | |
hierarchy_posts[current_name] = set() | |
hierarchy_posts[current_name].add(post) | |
add(type_config['hierarchy_tree'][lang], type_config['hierarchy_posts'][lang]) | |
add(type_config['hierarchy_tree_all'], type_config['hierarchy_posts_all']) | |
def _process_post_object(self, site, post): | |
for type, type_config in self._setup.items(): | |
names = {} | |
for lang in site.config['TRANSLATIONS']: | |
if not post.is_translation_available(lang) and not site.config['SHOW_UNTRANSLATED_POSTS']: | |
continue | |
# First aggregate values | |
values = set() | |
for source in type_config['sources']: | |
v = post.meta(source['identifier'], lang=lang).strip() | |
if source['can_specify_multiple']: | |
for vv in [vv.strip() for vv in v.split(',')]: | |
if vv: | |
values.add(vv) | |
else: | |
if v: | |
values.add(v) | |
# Convert to sorted list and ignore the ones to ignore | |
values_paths = [] | |
for value in values: | |
if value not in type_config['ignore']: | |
values_paths.append((value, self._parse_name(value, type_config))) | |
values_paths = natsort.natsorted(values_paths, key=lambda value: value[1][-1], alg=natsort.ns.F | natsort.ns.IC) | |
# Add to hierarchy (if post appears in feeds) | |
if post.use_in_feeds: | |
for value, path in values_paths: | |
self._add_to_hierarchy(post, value, path, type_config, lang) | |
# Store | |
names[lang] = [value for (value, path) in values_paths] | |
post.meta[lang]['{0}'.format(type_config['pluralname'])] = names[lang] | |
post.meta[lang]['{0}_paths'.format(type_config['pluralname'])] = [path for (value, path) in values_paths] | |
# Backwards compatibility | |
if type == 'category': | |
post.meta[lang][type] = names[lang][0] if names[lang] else '' | |
elif type == 'tag': | |
post._tags[lang] = names[lang] | |
# Add dependency to post | |
post.add_dependency_uptodate(utils.config_changed({'names-{0}'.format(lang): names[lang] for lang in names}, 'extended_tags:{0}:tags:{1}'.format(post.source_path, type)), False, 'page') | |
def _slugify_name_part(self, name, type_config): | |
"""Slugify a tag name part.""" | |
if type_config['slug_paths']: | |
name = utils.slugify(name) | |
return name | |
def _slugify_name(self, name, lang, type_config): | |
"""Slugify a tag name. Returns path.""" | |
path = self._parse_name(name, type_config) | |
if type_config['output_flat_hierarchy']: | |
path = path[-1:] # only the leaf | |
result = [self._slugify_name_part(part, type_config) for part in path] | |
result[0] = type_config['prefix'] + result[0] | |
if not self.site.config['PRETTY_URLS']: | |
result = ['-'.join(result)] | |
return result | |
def _process_posts_and_stories(self, site): | |
for type, type_config in self._setup.items(): | |
self._setup[type]['hierarchy_tree'] = {lang: {} for lang in site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_tree_all'] = {} | |
self._setup[type]['hierarchy_posts'] = {lang: {} for lang in site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_posts_all'] = {} | |
self._setup[type]['hierarchy_lookup'] = {lang: {} for lang in site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_lookup_all'] = {} | |
self._setup[type]['hierarchy'] = {lang: [] for lang in site.config['TRANSLATIONS']} | |
self._setup[type]['hierarchy_all'] = [] | |
try: | |
# Collect tags | |
for post in site.timeline: | |
self._process_post_object(site, post) | |
# Now process all tags | |
slugs = {lang: {} for lang in self.site.config['TRANSLATIONS']} # for finding collisions | |
for type, type_config in self._setup.items(): | |
def process(lang, hierarchy_posts, hierarchy_lookup, hierarchy_tree): | |
# First clean up post per name lists | |
for name in hierarchy_posts: | |
# Convert set to list | |
hierarchy_posts[name] = list(hierarchy_posts[name]) | |
# Sort by date | |
hierarchy_posts[name].sort(key=lambda a: a.date) | |
hierarchy_posts[name].reverse() | |
# Create hierarchy | |
def create_hierarchy(hierarchy, type, type_config, slugs, parent=None): | |
"""Create hierarchy and collect slugs.""" | |
result = [] | |
for name, children in hierarchy.items(): | |
node = utils.TreeNode(name, parent) | |
node.children = create_hierarchy(children, type, type_config, slugs, node) | |
node.tag_path = [pn.name for pn in node.get_path()] | |
node.tag_name = self._combine_path(node.tag_path, type_config) | |
hierarchy_lookup[node.tag_name] = node | |
if node.tag_name not in type_config['hidden']: | |
result.append(node) | |
return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC) | |
root_list = create_hierarchy(hierarchy_tree, type, type_config, slugs) | |
# Return tree and flattened hierarchy | |
return root_list, utils.flatten_tree_structure(root_list) | |
for lang in site.config['TRANSLATIONS']: | |
type_config['hierarchy_tree'][lang], type_config['hierarchy'][lang] = process(lang, type_config['hierarchy_posts'][lang], type_config['hierarchy_lookup'][lang], type_config['hierarchy_tree'][lang]) | |
type_config['hierarchy_tree_all'], type_config['hierarchy_all'] = process(None, type_config['hierarchy_posts_all'], type_config['hierarchy_lookup_all'], type_config['hierarchy_tree_all']) | |
# Update collision dict | |
for lang in site.config['TRANSLATIONS']: | |
posts_all = (type_config['hierarchy_posts'][lang] if type_config['only_tags_from_this_language'] else type_config['hierarchy_posts_all']) | |
for tag_name in posts_all: | |
if type_config['omit_empty_tags']: | |
posts = posts_all[tag_name] | |
if not self.site.config['SHOW_UNTRANSLATED_POSTS']: | |
posts = [post for post in posts if post.is_translation_available(lang)] | |
if len(posts) == 0: | |
continue | |
slug_index = tuple(type_config['path'][lang].split('/') + self._slugify_name(tag_name, lang, type_config)) | |
if slug_index not in slugs[lang]: | |
slugs[lang][slug_index] = [] | |
slugs[lang][slug_index].append((type, tag_name)) | |
# For backwards compatibility: | |
if type == 'category': | |
self.site.posts_per_category = type_config['hierarchy_posts_all'] | |
self.site.category_hierarchy = type_config['hierarchy_all'] | |
# Check for slug collisions | |
quit = False | |
for lang in sorted(slugs.keys()): | |
for slug, values in sorted(slugs[lang].items()): | |
if len(values) > 1: | |
utils.LOGGER.error('You have tags which are too similar and yield the same slug {0} for language {1}!'.format('/'.join(slug), lang)) | |
for value in values: | |
quit = True | |
posts = self._setup[value[0]]['hierarchy_posts_all'].get(value[1], []) | |
if not self.site.config['SHOW_UNTRANSLATED_POSTS']: | |
posts = [post for post in posts if post.is_translation_available(lang)] | |
post_sources = ', '.join([post.translated_source_path(lang) for post in posts]) | |
if not post_sources: | |
post_sources = '--' | |
utils.LOGGER.error(' "{1}" (of type "{0}") yields this slug; occuring in: {2}'.format(value[0], value[1], post_sources)) | |
if quit: | |
sys.exit(1) | |
except Exception as e: | |
traceback.print_exc(e) | |
_LOGGER.error(str(e)) | |
sys.exit(1) | |
@staticmethod | |
def _add_extension(path, extension): | |
path[-1] += extension | |
return path | |
def _tag_path_beginning(self, lang, type_config): | |
return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + type_config['path'][lang].split('/') if _f] | |
def _tag_index_path(self, lang, type_config): | |
"""Return path to the category index.""" | |
return self._tag_path_beginning(lang, type_config) + [self.site.config['INDEX_FILE']] | |
def _tag_path(self, name, lang, type_config): | |
"""Return path to a category.""" | |
if self.site.config['PRETTY_URLS']: | |
return self._tag_path_beginning(lang, type_config) + self._slugify_name(name, lang, type_config) + [self.site.config['INDEX_FILE']] | |
else: | |
return self._tag_path_beginning(lang, type_config) + self._add_extension(self._slugify_name(name, lang, type_config), ".html") | |
def _tag_atom_path(self, name, lang, type_config): | |
"""Return path to a category Atom feed.""" | |
return self._tag_path_beginning(lang, type_config) + self._add_extension(self._slugify_name(name, lang, type_config), ".atom") | |
def _tag_rss_path(self, name, lang, type_config): | |
"""Return path to a category RSS feed.""" | |
return self._tag_path_beginning(lang, type_config) + self._add_extension(self._slugify_name(name, lang, type_config), ".xml") | |
def _register_path_handlers(self, type, type_config): | |
self.site.register_path_handler('{0}_index'.format(type), lambda name, lang: self._tag_index_path(lang, type_config)) | |
self.site.register_path_handler('{0}'.format(type), lambda name, lang: self._tag_path(name, lang, type_config)) | |
self.site.register_path_handler('{0}_atom'.format(type), lambda name, lang: self._tag_atom_path(name, lang, type_config)) | |
self.site.register_path_handler('{0}_rss'.format(type), lambda name, lang: self._tag_rss_path(name, lang, type_config)) | |
def set_site(self, site): | |
"""Set site, which is a Nikola instance.""" | |
super(ExtendedTagsPreprocess, self).set_site(site) | |
# Parse config | |
self._parse_config(site.config) | |
# Store config in Nikola site object so that the extended_tags plugin an access it, too | |
self.site.extended_tags_setup = self._setup | |
self.site.extended_tags_parse_name = self._parse_name | |
self.site.extended_tags_combine_path = self._combine_path | |
# Add hook for after post scanning | |
blinker.signal("scanned").connect(self._process_posts_and_stories) | |
# Register path handlers | |
for type, type_config in self._setup.items(): | |
self._register_path_handlers(type, type_config) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment