Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b6e0971

Browse files
committedMay 10, 2015
Allowing category hierarchies. Must currently be explicitly enabled.
fixes #1520
1 parent 6360997 commit b6e0971

File tree

16 files changed

+348
-66
lines changed

16 files changed

+348
-66
lines changed
 

Diff for: ‎nikola/conf.py.in

+10
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ HIDDEN_TAGS = ['mathjax']
250250
# CATEGORY_PATH = "categories"
251251
# CATEGORY_PREFIX = "cat_"
252252

253+
# If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in
254+
# hierarchies. For a post, the whole path in the hierarchy must be specified,
255+
# using a forward slash ('/') to separate paths. Use a backslash ('\') to escape
256+
# a forward slash or a backslash (i.e. '\//\\' is a path specifying the
257+
# subcategory called '\' of the top-level category called '/').
258+
# CATEGORY_ALLOW_HIERARCHIES = False
259+
# If CATEGORY_OUTPUT_FLAT_HIERARCHY is set to True, the output written to output
260+
# contains only the name of the leaf category and not the whole path.
261+
# CATEGORY_OUTPUT_FLAT_HIERARCHY = False
262+
253263
# If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain
254264
# the posts themselves. If set to False, it will be just a list of links.
255265
# CATEGORY_PAGES_ARE_INDEXES = False

Diff for: ‎nikola/data/themes/base-jinja/templates/index.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
{% endblock %}
1212

1313
{% block content %}
14+
{% if subcategories: %}
15+
{{ messages('Subcategories:') }}
16+
<ul>
17+
{% for name, link in subcategories: %}
18+
<li><a href="{{ link }}">{{ name }}</a></li>
19+
{% endfor %}
20+
</ul>
21+
{% endif %}
1422
<div class="postindex">
1523
{% for post in posts %}
1624
<article class="h-entry post-{{ post.meta('type') }}">

Diff for: ‎nikola/data/themes/base-jinja/templates/tag.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
{% if description %}
2121
<p>{{ description }}</p>
2222
{% endif %}
23+
{% if subcategories: %}
24+
{{ messages('Subcategories:') }}
25+
<ul>
26+
{% for name, link in subcategories: %}
27+
<li><a href="{{ link }}">{{ name }}</a></li>
28+
{% endfor %}
29+
</ul>
30+
{% endif %}
2331
<div class="metadata">
2432
{% if translations|length > 1 and generate_rss %}
2533
{% for language in translations %}

Diff for: ‎nikola/data/themes/base-jinja/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010
{% if items %}
1111
<h2>{{ messages("Categories") }}</h2>
1212
{% endif %}
13-
<ul class="postlist">
14-
{% for text, link in cat_items %}
15-
{% if text and text not in hidden_categories %}
16-
<li><a class="reference" href="{{ link }}">{{ text }}</a></li>
13+
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy: %}
14+
{% for i in range(indent_change_before): %}
15+
<ul class="postlist">
16+
{% endfor %}
17+
<li><a class="reference" href="{{ link }}">{{ text }}</a>
18+
{% if indent_change_after <= 0: %}
19+
</li>
1720
{% endif %}
21+
{% for i in range(-indent_change_after): %}
22+
</ul>
23+
% if i + 1 < len(indent_levels): %}
24+
</li>
25+
% endif %}
26+
{% endfor %}
1827
{% endfor %}
19-
</ul>
2028
{% if items %}
2129
<h2>{{ messages("Tags") }}</h2>
2230
{% endif %}

Diff for: ‎nikola/data/themes/base/messages/messages_de.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"Read more": "Weiterlesen",
2929
"Skip to main content": "Springe zum Hauptinhalt",
3030
"Source": "Source",
31+
"Subcategories:": "Unterkategorien:",
3132
"Tags and Categories": "Tags und Kategorien",
3233
"Tags": "Tags",
3334
"old posts, page %d": "Ältere Einträge, Seite %d",

Diff for: ‎nikola/data/themes/base/messages/messages_en.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"Read more": "Read more",
2929
"Skip to main content": "Skip to main content",
3030
"Source": "Source",
31+
"Subcategories:": "Subcategories:",
3132
"Tags and Categories": "Tags and Categories",
3233
"Tags": "Tags",
3334
"old posts, page %d": "old posts, page %d",

Diff for: ‎nikola/data/themes/base/templates/index.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
</%block>
1212

1313
<%block name="content">
14+
%if subcategories:
15+
${messages('Subcategories:')}
16+
<ul>
17+
%for name, link in subcategories:
18+
<li><a href="${link}">${name}</a></li>
19+
%endfor
20+
</ul>
21+
%endif
1422
<div class="postindex">
1523
% for post in posts:
1624
<article class="h-entry post-${post.meta('type')}">

Diff for: ‎nikola/data/themes/base/templates/tag.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
%if description:
2121
<p>${description}</p>
2222
%endif
23+
%if subcategories:
24+
${messages('Subcategories:')}
25+
<ul>
26+
%for name, link in subcategories:
27+
<li><a href="${link}">${name}</a></li>
28+
%endfor
29+
</ul>
30+
%endif
2331
<div class="metadata">
2432
%if len(translations) > 1 and generate_rss:
2533
%for language in translations:

Diff for: ‎nikola/data/themes/base/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010
% if items:
1111
<h2>${messages("Categories")}</h2>
1212
% endif
13-
<ul class="postlist">
14-
% for text, link in cat_items:
15-
% if text and text not in hidden_categories:
16-
<li><a class="reference" href="${link}">${text}</a></li>
13+
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
14+
% for i in range(indent_change_before):
15+
<ul class="postlist">
16+
% endfor
17+
<li><a class="reference" href="${link}">${text}</a>
18+
% if indent_change_after <= 0:
19+
</li>
1720
% endif
21+
% for i in range(-indent_change_after):
22+
</ul>
23+
% if i + 1 < len(indent_levels):
24+
</li>
25+
% endif
26+
% endfor
1827
% endfor
19-
</ul>
2028
% if items:
2129
<h2>${messages("Tags")}</h2>
2230
% endif

Diff for: ‎nikola/data/themes/bootstrap-jinja/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
{% if items %}
88
<h2>{{ messages("Categories") }}</h2>
99
{% endif %}
10-
<ul class="unstyled">
11-
{% for text, link in cat_items %}
12-
{% if text and text not in hidden_categories %}
13-
<li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
10+
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy: %}
11+
{% for i in range(indent_change_before): %}
12+
<ul class="unstyled">
13+
{% endfor %}
14+
<li><a class="reference badge" href="{{ link }}">{{ text }}</a>
15+
{% if indent_change_after <= 0: %}
16+
</li>
1417
{% endif %}
18+
{% for i in range(-indent_change_after): %}
19+
</ul>
20+
{% if i + 1 < len(indent_levels): %}
21+
</li>
22+
{% endif %}
23+
{% endfor %}
1524
{% endfor %}
16-
</ul>
1725
{% if items %}
1826
<h2>{{ messages("Tags") }}</h2>
1927
{% endif %}

Diff for: ‎nikola/data/themes/bootstrap/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
% if items:
88
<h2>${messages("Categories")}</h2>
99
% endif
10-
<ul class="unstyled">
11-
% for text, link in cat_items:
12-
% if text and text not in hidden_categories:
13-
<li><a class="reference badge" href="${link}">${text}</a></li>
10+
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
11+
% for i in range(indent_change_before):
12+
<ul class="unstyled">
13+
% endfor
14+
<li><a class="reference badge" href="${link}">${text}</a>
15+
% if indent_change_after <= 0:
16+
</li>
1417
% endif
18+
% for i in range(-indent_change_after):
19+
</ul>
20+
% if i + 1 < len(indent_levels):
21+
</li>
22+
% endif
23+
% endfor
1524
% endfor
16-
</ul>
1725
% if items:
1826
<h2>${messages("Tags")}</h2>
1927
% endif

Diff for: ‎nikola/nikola.py

+79-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import os
3636
import json
3737
import sys
38+
import natsort
3839
import mimetypes
3940
try:
4041
from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote
@@ -317,6 +318,8 @@ def __init__(self, **config):
317318
'CATEGORY_PAGES_ARE_INDEXES': None, # None means: same as TAG_PAGES_ARE_INDEXES
318319
'CATEGORY_PAGES_DESCRIPTIONS': {},
319320
'CATEGORY_PREFIX': 'cat_',
321+
'CATEGORY_ALLOW_HIERARCHIES': False,
322+
'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
320323
'CODE_COLOR_SCHEME': 'default',
321324
'COMMENT_SYSTEM': 'disqus',
322325
'COMMENTS_IN_GALLERIES': False,
@@ -1352,6 +1355,79 @@ def flatten(task):
13521355
'task_dep': task_dep
13531356
}
13541357

1358+
def parse_category_name(self, category_name):
1359+
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
1360+
result = []
1361+
current = ''
1362+
index = 0
1363+
next_backslash = category_name.find('\\', index)
1364+
next_slash = category_name.find('/', index)
1365+
while index < len(category_name):
1366+
if next_backslash == -1 and next_slash == -1:
1367+
current += category_name[index:]
1368+
index = len(category_name)
1369+
elif next_slash >= 0 and (next_backslash == -1 or next_backslash > next_slash):
1370+
result.append(current + category_name[index:next_slash])
1371+
current = ''
1372+
index = next_slash + 1
1373+
next_slash = category_name.find('/', index)
1374+
else:
1375+
if len(category_name) == next_backslash + 1:
1376+
utils.LOGGER.error("Unexpected '\\' in '{0}' at last position!".format(category_name))
1377+
sys.exit(1)
1378+
esc_ch = category_name[next_backslash + 1]
1379+
if esc_ch not in {'/', '\\'}:
1380+
utils.LOGGER.error("Unknown escape sequence '\\{0}' in '{1}' at last position!".format(esc_ch, category_name))
1381+
sys.exit(1)
1382+
current += category_name[index:next_backslash] + esc_ch
1383+
index = next_backslash + 2
1384+
next_backslash = category_name.find('\\', index)
1385+
if esc_ch == '/':
1386+
next_slash = category_name.find('/', index)
1387+
if len(current) > 0:
1388+
result.append(current)
1389+
return result
1390+
else:
1391+
return [category_name] if len(category_name) > 0 else []
1392+
1393+
def category_path_to_category_name(self, category_path):
1394+
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
1395+
def escape(s):
1396+
return s.replace('\\', '\\\\').replace('/', '\\/')
1397+
1398+
return '/'.join([escape(p) for p in category_path])
1399+
else:
1400+
return ''.join(category_path)
1401+
1402+
def _add_post_to_category(self, post, category_name):
1403+
category_path = self.parse_category_name(category_name)
1404+
current_path = []
1405+
current_subtree = self.category_hierarchy
1406+
for current in category_path:
1407+
current_path.append(current)
1408+
if current not in current_subtree:
1409+
current_subtree[current] = {}
1410+
current_subtree = current_subtree[current]
1411+
self.posts_per_category[self.category_path_to_category_name(current_path)].append(post)
1412+
1413+
def _sort_category_hierarchy(self):
1414+
self.category_hierarchy_lookup = {}
1415+
# First create a hierarchy of TreeNodes
1416+
def create_hierarchy(cat_hierarchy, parent=None):
1417+
result = []
1418+
for name, children in cat_hierarchy.items():
1419+
node = utils.TreeNode(name, parent)
1420+
node.children = create_hierarchy(children, node)
1421+
node.category_path = [pn.name for pn in node.get_path()]
1422+
node.category_name = self.category_path_to_category_name(node.category_path)
1423+
self.category_hierarchy_lookup[node.category_name] = node
1424+
if node.category_name not in self.config.get('HIDDEN_CATEGORIES'):
1425+
result.append(node)
1426+
return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC)
1427+
root_list = create_hierarchy(self.category_hierarchy)
1428+
# Next, flatten the hierarchy
1429+
self.category_hierarchy = utils.flatten_tree_structure(root_list)
1430+
13551431
def scan_posts(self, really=False, ignore_quit=False, quiet=False):
13561432
"""Scan all the posts."""
13571433
if self._scanned and not really:
@@ -1364,6 +1440,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
13641440
self.posts_per_month = defaultdict(list)
13651441
self.posts_per_tag = defaultdict(list)
13661442
self.posts_per_category = defaultdict(list)
1443+
self.category_hierarchy = {}
13671444
self.post_per_file = {}
13681445
self.timeline = []
13691446
self.pages = []
@@ -1441,7 +1518,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
14411518
else:
14421519
slugged_tags.add(utils.slugify(tag, force=True))
14431520
self.posts_per_tag[tag].append(post)
1444-
self.posts_per_category[post.meta('category')].append(post)
1521+
self._add_post_to_category(post, post.meta('category'))
14451522

14461523
if post.is_post:
14471524
# unpublished posts
@@ -1462,6 +1539,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
14621539
self.all_posts.reverse()
14631540
self.pages.sort(key=lambda p: p.date)
14641541
self.pages.reverse()
1542+
self._sort_category_hierarchy()
14651543

14661544
for i, p in enumerate(self.posts[1:]):
14671545
p.next_post = self.posts[i]

0 commit comments

Comments
 (0)
Please sign in to comment.