Skip to content

Instantly share code, notes, and snippets.

@felixfontein
Last active September 21, 2015 20:28
Show Gist options
  • Save felixfontein/38468f5d34cedd5148e6 to your computer and use it in GitHub Desktop.
Save felixfontein/38468f5d34cedd5148e6 to your computer and use it in GitHub Desktop.
Extended tags plugins for Nikola.
[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.
"""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'])
[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.
"""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