Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Adds THEME_COLORS, POSTS_CATEGORIES, POSTS_CATEGORY_*, and HUSL color…
… functions

Issue #1980
  • Loading branch information
da2x committed Aug 27, 2015
1 parent 3d89be0 commit b6f2eee
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 1 deletion.
11 changes: 11 additions & 0 deletions CHANGES.txt
Expand Up @@ -4,6 +4,17 @@ New in master
Features
--------

* New ``THEME_COLOR`` option for customizing themes from a primary color
(Issue #1980)
* New ``POSTS`` output subfolders now generate categories by deault
(Issue #1980)
* New ``POSTS_CATEGORIES`` and ``POSTS_CATEGORY_*`` options for
configuring the new category pages (Issue #1980)
* For themers: Each ``post`` are now asssociated with category_color,
category_link, and category_name (Issue #1980)
* Each new category page has a auto-assigned color based on shifting
the hue of ``THEME_COLOR`` based on a hash of the category name,
can be overwritten with ``POSTS_CATEGORY_COLORS`` option (Issue #1980)
* New ``TAG_PAGES_TITLES`` and ``CATEGORY_PAGES_TITLES`` options
(Issue #1962)

Expand Down
46 changes: 46 additions & 0 deletions nikola/conf.py.in
Expand Up @@ -89,6 +89,10 @@ NAVIGATION_LINKS = ${NAVIGATION_LINKS}
# Name of the theme to use.
THEME = ${THEME}

# Primary color of your theme. This will be used to customize your theme and
# auto-generate related colors in POSTS_CATEGORY_COLORS. Must be a HEX value.
THEME_COLOR = '#5670d4'

##############################################
# Below this point, everything is optional
##############################################
Expand Down Expand Up @@ -208,6 +212,48 @@ COMPILERS = ${COMPILERS}
# Warning: this option will change its default value to False in v8!
WRITE_TAG_CLOUD = True

# Generate pages for categories. The site must have at least two categories
# for this option to take effect. It wouldn't build for just one category.
POSTS_CATEGORIES = True

# Setting this to False generates a list page instead of an index. Indexes
# are the default and will apply GENERATE_ATOM if set.
# POSTS_CATEGORY_ARE_INDEXES = True

# Each post and category page will have an associated color that can be used
# to style them with a recognizable color detail across your site. A color
# is assigned to each category based on shifting the hue of your THEME_COLOR
# at least 7.5 % while leaving the lightness and saturation untouched in the
# HUSL colorspace. You can overwrite colors by assigning them colors in HEX.
POSTS_CATEGORY_COLORS = {
DEFAULT_LANG: {
'posts': '#49b11bf',
'reviews': '#ffe200',
},
}

# Associate a description with a category. For use in meta description on
# category index pages or elsewhere in themes.
POSTS_CATEGORY_DESCRIPTIONS = {
DEFAULT_LANG: {
'how-to': 'Learn how-to things properly with these amazing tutorials.',
},
}

# Categories are determined by their output directory set in POSTS by default,
# but can alternatively be determined from file metadata instead.
# POSTS_CATEGORY_FROM_META = False

# Names are determined from the output directory name automatically or the
# metadata label. Unless overwritten below, names will use title cased and
# hyphens replaced by spaces.
# POSTS_CATEGORY_NAME = {
# DEFAULT_LANG: {
# 'posts': 'Blog Posts',
# 'uncategorized': 'Odds and Ends',
# },
# }

# Paths for different autogenerated bits. These are combined with the
# translation paths.

Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/base-jinja/templates/base_helper.tmpl
Expand Up @@ -33,6 +33,7 @@ lang="{{ lang }}">
{% endif %}

{{ html_stylesheets() }}
<meta content="{{ theme_color }}" name="theme-color">
{{ html_feedlinks() }}
<link rel="canonical" href="{{ abs_link(permalink) }}">

Expand Down
21 changes: 21 additions & 0 deletions nikola/data/themes/base-jinja/templates/categoryindex.tmpl
@@ -0,0 +1,21 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}

{% block extra_head %}
{{ super() }}
{% if generate_atom %}
<link rel="alternate" type="application/atom+xml" title="Atom for the {{ posts[0].category_name() }} section" href="/FIXME">
{% endif %}
{% endblock %}

{% block content %}
<div class="categoryindex">
<header style="background: {{ posts[0].category_color() }}">
<h2><a href="{{ posts[0].category_link() }}">{{ posts[0].category_name() }}</a></h2>
{% if generate_atom %}
<p class="feedlink"><a href="{{ _link('cat_index_atom', posts[0].category_slug()) }}" type="application/atom+xml">{{ messages('updates') }}</a></p>
{% endif %}
</header>
{{ parent.content() }}
</article>
{% endblock %}
2 changes: 2 additions & 0 deletions nikola/data/themes/base/messages/messages_en.py
Expand Up @@ -36,4 +36,6 @@
"Write your post here.": "Write your post here.",
"old posts, page %d": "old posts, page %d",
"page %d": "page %d",
"uncategorized": "Uncategorized",
"updates": "Updates"
}
1 change: 1 addition & 0 deletions nikola/data/themes/base/templates/base_helper.tmpl
Expand Up @@ -33,6 +33,7 @@ lang="${lang}">
%endif

${html_stylesheets()}
<meta content="${theme_color}" name="theme-color">
${html_feedlinks()}
<link rel="canonical" href="${abs_link(permalink)}">

Expand Down
21 changes: 21 additions & 0 deletions nikola/data/themes/base/templates/categoryindex.tmpl
@@ -0,0 +1,21 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>

<%block name="extra_head">
${parent.extra_head()}
% if generate_atom:
<link rel="alternate" type="application/atom+xml" title="Atom for the ${posts[0].category_name()} section" href="/FIXME">
% endif
</%block>

<%block name="content">
<div class="categoryindex">
<header style="background: ${posts[0].category_color()}">
<h2><a href="${posts[0].category_link()}">${posts[0].category_name()}</a></h2>
% if generate_atom:
<p class="feedlink"><a href="${_link('cat_index_atom', posts[0].category_slug())}" type="application/atom+xml">${messages('updates')}</a></p>
% endif
</header>
${parent.content()}
</article>
</%block>
Expand Up @@ -38,6 +38,7 @@ lang="{{ lang }}">
{% endif %}

{{ html_stylesheets() }}
<meta content="{{ theme_color }}" name="theme-color">
{{ html_feedlinks() }}
<link rel="canonical" href="{{ abs_link(permalink) }}">

Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/bootstrap3/templates/base_helper.tmpl
Expand Up @@ -38,6 +38,7 @@ lang="${lang}">
%endif

${html_stylesheets()}
<meta content="${theme_color}" name="theme-color">
${html_feedlinks()}
<link rel="canonical" href="${abs_link(permalink)}">

Expand Down
13 changes: 12 additions & 1 deletion nikola/nikola.py
Expand Up @@ -418,6 +418,12 @@ def __init__(self, **config):
'OLD_THEME_SUPPORT': True,
'OUTPUT_FOLDER': 'output',
'POSTS': (("posts/*.txt", "posts", "post.tmpl"),),
'POSTS_CATEGORIES': True,
'POSTS_CATEGORY_ARE_INDEXES': True,
'POSTS_CATEGORY_DESCRIPTIONS': "",
'POSTS_CATEGORY_FROM_META': False,
'POSTS_CATEGORY_NAME': "",
'POSTS_CATEGORY_TITLE': "%s",
'PAGES': (("stories/*.txt", "stories", "story.tmpl"),),
'PANDOC_OPTIONS': [],
'PRETTY_URLS': False,
Expand Down Expand Up @@ -452,6 +458,7 @@ def __init__(self, **config):
'TAGLIST_MINIMUM_POSTS': 1,
'TEMPLATE_FILTERS': {},
'THEME': 'bootstrap3',
'THEME_COLOR': '#5670d4', # light "corporate blue"
'THEME_REVEAL_CONFIG_SUBTHEME': 'sky',
'THEME_REVEAL_CONFIG_TRANSITION': 'cube',
'THUMBNAIL_SIZE': 180,
Expand Down Expand Up @@ -514,6 +521,10 @@ def __init__(self, **config):
'INDEX_READ_MORE_LINK',
'RSS_READ_MORE_LINK',
'INDEXES_TITLE',
'POSTS_CATEGORY_COLORS',
'POSTS_CATEGORY_DESCRIPTIONS',
'POSTS_CATEGORY_NAME',
'POSTS_CATEGORY_TITLE',
'INDEXES_PAGES',
'INDEXES_PRETTY_PAGE_URL',)

Expand Down Expand Up @@ -837,6 +848,7 @@ def _set_global_context(self):
self._GLOBAL_CONTEXT['index_file'] = self.config['INDEX_FILE']
self._GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
self._GLOBAL_CONTEXT['theme_color'] = self.config.get("THEME_COLOR")
self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
self._GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT')
self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
Expand Down Expand Up @@ -1310,7 +1322,6 @@ def path(self, kind, name, lang=None, is_link=False):
try:
path = self.path_handlers[kind](name, lang)
path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028

if is_link:
link = '/' + ('/'.join(path))
index_len = len(self.config['INDEX_FILE'])
Expand Down
85 changes: 85 additions & 0 deletions nikola/plugins/task/indexes.py
Expand Up @@ -29,11 +29,16 @@
from __future__ import unicode_literals
from collections import defaultdict
import os
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoin # NOQA

from nikola.plugin_categories import Task
from nikola import utils



class Indexes(Task):

"""Render the blog indexes."""
Expand All @@ -44,6 +49,8 @@ def set_site(self, site):
"""Set Nikola site."""
site.register_path_handler('index', self.index_path)
site.register_path_handler('index_atom', self.index_atom_path)
site.register_path_handler('cat_index', self.index_cat_path)
site.register_path_handler('cat_index_atom', self.index_cat_atom_path)
return super(Indexes, self).set_site(site)

def gen_tasks(self):
Expand All @@ -56,16 +63,19 @@ def gen_tasks(self):
"messages": self.site.MESSAGES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"index_file": self.site.config['INDEX_FILE'],
"show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
"index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'],
"indexes_title": self.site.config['INDEXES_TITLE'],
"strip_indexes": self.site.config['STRIP_INDEXES'],
"blog_title": self.site.config["BLOG_TITLE"],
"generate_atom": self.site.config["GENERATE_ATOM"],
}

template_name = "index.tmpl"
posts = self.site.posts
self.number_of_pages = dict()
self.number_of_pages_cat = dict()
for lang in kw["translations"]:
def page_link(i, displayed_i, num_pages, force_addition, extension=None):
feed = "_atom" if extension == ".atom" else ""
Expand All @@ -90,6 +100,59 @@ def page_path(i, displayed_i, num_pages, force_addition, extension=None):

yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path)

if self.site.config['POSTS_CATEGORIES']:

kw["posts_category_are_indexes"] = self.site.config['POSTS_CATEGORY_ARE_INDEXES']
kw["posts_category_title"] = self.site.config['POSTS_CATEGORY_TITLE'](lang)

index_len = len(kw['index_file'])

groups = defaultdict(list)
for p in filtered_posts:
groups[p.category_slug(lang)].append(p)

# don't build categories when there is only one, aka. default setups
if not len(groups.items()) > 1:
continue

for dirname, post_list in groups.items():

if not lang in self.number_of_pages_cat:
self.number_of_pages_cat[lang] = dict()
self.number_of_pages_cat[lang][dirname] = (len(post_list) + kw['index_display_post_count'] - 1) // kw['index_display_post_count']

def cat_link(i, displayed_i, num_pages, force_addition, extension=None):
feed = "_atom" if extension == ".atom" else ""
return utils.adjust_name_for_index_link(self.site.link("cat_index" + feed, dirname, lang), i, displayed_i,
lang, self.site, force_addition, extension)

def cat_path(i, displayed_i, num_pages, force_addition, extension=None):
feed = "_atom" if extension == ".atom" else ""
return utils.adjust_name_for_index_path(self.site.path("cat_index" + feed, dirname, lang), i, displayed_i,
lang, self.site, force_addition, extension)

context = {}

short_destination = os.path.join(dirname, kw['index_file'])
link = short_destination.replace('\\', '/')
if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
link = link[:-index_len]
context["permalink"] = link
context["pagekind"] = ["posts_category_page"]
context["description"] = self.site.config['POSTS_CATEGORY_DESCRIPTIONS'](lang)[dirname] if dirname in self.site.config['POSTS_CATEGORY_DESCRIPTIONS'](lang) else ""

if kw["posts_category_are_indexes"]:
context["pagekind"].append("index")
indexes_title = post_list[0].category_name(lang) # all posts in the list share category name
task = self.site.generic_index_renderer(lang, post_list, indexes_title, "categoryindex.tmpl", context, kw, self.name, cat_link, cat_path)
else:
context["pagekind"].append("list")
output_name = os.path.join(kw['output_folder'], dirname, kw['index_file'])
task = self.site.generic_post_list_renderer(lang, post_list, output_name, "list.tmpl", kw['filters'], context)
task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
task['basename'] = self.name
yield task

if not self.site.config["STORY_INDEX"]:
return
kw = {
Expand Down Expand Up @@ -163,6 +226,28 @@ def index_path(self, name, lang, is_feed=False):
self.site,
extension=extension)

def index_cat_path(self, name, lang, is_feed=False):
"""Return path to an index."""
extension = None

if is_feed:
extension = ".atom"
index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
else:
index_file = self.site.config['INDEX_FILE']
return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang],
name,
index_file] if _f],
None,
utils.get_displayed_page_number(None, self.number_of_pages_cat[lang][name], self.site),
lang,
self.site,
extension=extension)

def index_atom_path(self, name, lang):
"""Return path to an Atom index."""
return self.index_path(name, lang, is_feed=True)

def index_cat_atom_path(self, name, lang):
"""Return path to an Atom index for categories."""
return self.index_cat_path(name, lang, is_feed=True)
36 changes: 36 additions & 0 deletions nikola/post.py
Expand Up @@ -745,6 +745,42 @@ def destination_path(self, lang=None, extension='.html', sep=os.sep):
path = path[2:]
return path

def category_color(self, lang=None):
slug = self.category_slug(lang)
if slug in self.config['POSTS_CATEGORY_COLORS'](lang):
return self.config['POSTS_CATEGORY_COLORS'](lang)[slug]
base = self.config['THEME_COLOR']
return utils.colorize_str_from_base_color(slug, base)

def category_link(self, lang=None):
slug = self.category_slug(lang)
if not self.pretty_urls:
link = urljoin('/'+ slug + '/', self.index_file)
else:
link = '/' + slug + '/'
return link

def category_name(self, lang=None):
slug = self.category_slug(lang)
if slug in self.config['POSTS_CATEGORY_NAME'](lang):
name = self.config['POSTS_CATEGORY_NAME'](lang)[slug]
else:
name = slug.replace('-', ' ').title()
return name

def category_slug(self, lang=None):
if not self.config['POSTS_CATEGORY_FROM_META']:
dest = self.destination_path(lang)
if dest[-(1 + len(self.index_file)):] == '/' + self.index_file:
dest = dest[:-(1 + len(self.index_file))]
dirname = os.path.dirname(dest)
slug = dirname
if not dirname or dirname == '.':
slug = self.messages[lang]["uncategorized"]
else:
slug = self.meta[lang]['category'].split(',')[0] if 'category' in self.meta[lang] else self.messages[lang]["uncategorized"]
return slug

def permalink(self, lang=None, absolute=False, extension='.html', query=None):
"""Return permalink for a post."""
if lang is None:
Expand Down

0 comments on commit b6f2eee

Please sign in to comment.