Skip to content
Browse files
  • Loading branch information
felixfontein committed Aug 4, 2016
1 parent e8b2064 commit 78c59a3cb88f6489ba3314b321d99a00d4e2aa24
Showing 10 changed files with 50 additions and 39 deletions.
@@ -7,6 +7,8 @@ Features
* Add ``sections`` filtering in the post list directive
(Issue #2409)
* Update Bootstrap to v3.3.7
* Add ``EXTRA_THEMES_DIRS`` search path, similar to ``EXTRA_PLUGINS_DIRS``,
to locate themes at other places (Issue #2427)

@@ -351,7 +351,7 @@ And the ```` file, in its entirety:

tasks = {}
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
src = os.path.join(utils.get_theme_path(theme_name,, 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
if task['name'] in tasks:
@@ -438,6 +438,7 @@ def __init__(self, **config):
'DEPLOY_COMMANDS': {'default': []},
'COMMENT_SYSTEM_ID': 'nikolademo',
@@ -855,6 +856,9 @@ def __init__(self, **config):
candidate = utils.get_translation_candidate(self.config, "f" + ext, lang)

# Get search path for themes
self.themes_dirs = ['themes'] + self.config['EXTRA_THEMES_DIRS']

# Avoid redundant compilers
# Remove compilers that match nothing in POSTS/PAGES
# And put them in "bad compilers"
@@ -1113,7 +1117,7 @@ def _activate_plugins_of_category(self, category):
def _get_themes(self):
if self._THEMES is None:
self._THEMES = utils.get_theme_chain(self.config['THEME'])
self._THEMES = utils.get_theme_chain(self.config['THEME'], self.themes_dirs)
except Exception:
if self.config['THEME'] != 'bootstrap3':
utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap3' instead.'''.format(self.config['THEME']))
@@ -1123,7 +1127,7 @@ def _get_themes(self):
# Check consistency of USE_CDN and the current THEME (Issue #386)
if self.config['USE_CDN'] and self.config['USE_CDN_WARNING']:
bootstrap_path = utils.get_asset_path(os.path.join(
'assets', 'css', 'bootstrap.min.css'), self._THEMES)
'assets', 'css', 'bootstrap.min.css'), self._THEMES, themes_dirs=self.themes_dirs)
if bootstrap_path and bootstrap_path.split(os.sep)[-4] not in ['bootstrap', 'bootstrap3']:
utils.LOGGER.warn('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.')

@@ -1136,7 +1140,8 @@ def _get_messages(self):
if self._MESSAGES is None:
self._MESSAGES = utils.load_messages(self.THEMES,
return self._MESSAGES
except utils.LanguageNotFoundError as e:
utils.LOGGER.error('''Cannot load language "{0}". Please make sure it is supported by Nikola itself, or that you have the appropriate messages files in your themes.'''.format(e.lang))
@@ -1153,7 +1158,8 @@ def _get_global_context(self):
custom_css_path = utils.get_asset_path(
if custom_css_path and self.file_exists(custom_css_path, not_empty=True):
self._GLOBAL_CONTEXT['has_custom_css'] = True
@@ -1167,15 +1173,15 @@ def _get_global_context(self):
def _get_template_system(self):
if self._template_system is None:
# Load template plugin
template_sys_name = utils.get_template_engine(self.THEMES)
template_sys_name = utils.get_template_engine(self.THEMES, self.themes_dirs)
pi = self.plugin_manager.getPluginByName(
template_sys_name, "TemplateSystem")
if pi is None:
sys.stderr.write("Error loading {0} template system "
self._template_system = pi.plugin_object
lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name), "templates")
lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name, self.themes_dirs), "templates")
for name in self.THEMES]
@@ -1416,7 +1422,7 @@ def _register_templated_shortcodes(self):
"""Register shortcodes provided by templates in shortcodes/ folders."""
builtin_sc_dir = resource_filename(
os.path.join('data', 'shortcodes', utils.get_template_engine(self.THEMES)))
os.path.join('data', 'shortcodes', utils.get_template_engine(self.THEMES, self.themes_dirs)))

for sc_dir in [builtin_sc_dir, 'shortcodes']:
if not os.path.isdir(sc_dir):
@@ -1900,7 +1906,7 @@ def generic_page_renderer(self, lang, post, filters, context=None):
context = context.copy() if context else {}
deps = post.deps(lang) + \
deps.extend(utils.get_asset_path(x, self.THEMES) for x in ('bundles', 'parent', 'engine'))
deps.extend(utils.get_asset_path(x, self.THEMES, themes_dirs=self.themes_dirs) for x in ('bundles', 'parent', 'engine'))
deps = list(filter(None, deps))
context['post'] = post
context['lang'] = lang
@@ -158,7 +158,7 @@ def _execute(self, options, args):
# Do not duplicate entries -- otherwise, multiple rebuilds are triggered
watched = set([
] + [get_theme_path(name) for name in])
] + [get_theme_path(name, for name in])
for item in['post_pages']:
for item in['FILES_FOLDERS']:
@@ -79,7 +79,7 @@ def _execute(self, options, args):
version = ''

# See if we need bootswatch for bootstrap v2 or v3
themes = utils.get_theme_chain(parent)
themes = utils.get_theme_chain(parent,
if 'bootstrap3' not in themes and 'bootstrap3-jinja' not in themes:
version = '2'
elif 'bootstrap' not in themes and 'bootstrap-jinja' not in themes:
@@ -170,11 +170,11 @@ def do_install_deps(self, url, name):
installstatus = self.do_install(name, data)
# See if the theme's parent is available. If not, install it
while True:
parent_name = utils.get_parent_theme_name(name)
parent_name = utils.get_parent_theme_name(name,
if parent_name is None:
except: # Not available
self.do_install(parent_name, data)
@@ -204,7 +204,7 @@ def do_install(self, name, data):
dest_path = os.path.join(self.output_dir, name)
theme_path = utils.get_theme_path(name)
theme_path = utils.get_theme_path(name,
LOGGER.error("Theme '{0}' is already installed in {1}".format(name, theme_path))
except Exception:
LOGGER.error("Can't find theme {0}".format(name))
@@ -227,7 +227,7 @@ def do_install(self, name, data):
def do_uninstall(self, name):
"""Uninstall a theme."""
path = utils.get_theme_path(name)
path = utils.get_theme_path(name,
except Exception:
LOGGER.error('Unknown theme: {0}'.format(name))
return 1
@@ -243,7 +243,7 @@ def do_uninstall(self, name):
def get_path(self, name):
"""Get path for an installed theme."""
path = utils.get_theme_path(name)
path = utils.get_theme_path(name,
except Exception:
print("not installed")
@@ -268,7 +268,7 @@ def copy_template(self, template):

# Figure out where to put it.
# Check if a local theme exists.
theme_path = utils.get_theme_path([0])
theme_path = utils.get_theme_path([0],
if theme_path.startswith('themes' + os.sep):
# Theme in local themes/ directory
base = os.path.join(theme_path, 'templates')
@@ -297,7 +297,7 @@ def new_theme(self, name, engine, parent):"Created directory {0}".format(base))

# Check if engine and parent match
engine_file = utils.get_asset_path('engine', utils.get_theme_chain(parent))
engine_file = utils.get_asset_path('engine', utils.get_theme_chain(parent,,
with, 'r', encoding='utf-8') as fh:
parent_engine =

@@ -60,7 +60,7 @@ def gen_tasks(self):
'theme_bundles': get_theme_bundles(,
'theme_bundles': get_theme_bundles(,,
@@ -104,6 +104,7 @@ def build_bundle(output, inputs):
output_dir=kw['output_folder']) or fname == os.path.join('assets', 'css', 'code.css')]
# code.css will be generated by us if it does not exist in
# FILES_FOLDERS or theme assets. It is guaranteed that the
@@ -125,12 +126,12 @@ def build_bundle(output, inputs):
yield utils.apply_filters(task, kw['filters'])

def get_theme_bundles(themes):
def get_theme_bundles(themes, themes_dirs):
"""Given a theme chain, return the bundle definitions."""
bundles = {}
for theme_name in themes:
bundles_path = os.path.join(
utils.get_theme_path(theme_name), 'bundles')
utils.get_theme_path(theme_name, themes_dirs), 'bundles')
if os.path.isfile(bundles_path):
with open(bundles_path) as fd:
for line in fd:
@@ -60,11 +60,12 @@ def gen_tasks(self):
code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
code_css_input = utils.get_asset_path('assets/css/code.css',
files_folders=kw['files_folders'], output_dir=None)
files_folders=kw['files_folders'],, output_dir=None)
yield self.group_task()

for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
src = os.path.join(utils.get_theme_path(theme_name,, 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
if task['name'] in tasks:
@@ -71,7 +71,7 @@ def write_robots():

yield self.group_task()

if not utils.get_asset_path("robots.txt", [], files_folders=kw["files_folders"], output_dir=False):
if not utils.get_asset_path("robots.txt", [], files_folders=kw["files_folders"],, output_dir=False):
yield utils.apply_filters({
"name": robots_path,
@@ -572,46 +572,47 @@ def __repr__(self):

def get_theme_path(theme, _themes_dir='themes'):
def get_theme_path(theme, themes_dirs=['themes']):
"""Return the path where the given theme's files are located.
Looks in ./themes and in the place where themes go when installed.
dir_name = os.path.join(_themes_dir, theme)
if os.path.isdir(dir_name):
return dir_name
for themes_dir in themes_dirs:
dir_name = os.path.join(themes_dir, theme)
if os.path.isdir(dir_name):
return dir_name
dir_name = resource_filename('nikola', os.path.join('data', 'themes', theme))
if os.path.isdir(dir_name):
return dir_name
raise Exception("Can't find theme '{0}'".format(theme))

def get_template_engine(themes, _themes_dir='themes'):
def get_template_engine(themes, themes_dirs=['themes']):
"""Get template engine used by a given theme."""
for theme_name in themes:
engine_path = os.path.join(get_theme_path(theme_name, _themes_dir), 'engine')
engine_path = os.path.join(get_theme_path(theme_name, themes_dirs), 'engine')
if os.path.isfile(engine_path):
with open(engine_path) as fd:
return fd.readlines()[0].strip()
# default
return 'mako'

def get_parent_theme_name(theme_name, _themes_dir='themes'):
def get_parent_theme_name(theme_name, themes_dirs=['themes']):
"""Get name of parent theme."""
parent_path = os.path.join(get_theme_path(theme_name, _themes_dir), 'parent')
parent_path = os.path.join(get_theme_path(theme_name, themes_dirs), 'parent')
if os.path.isfile(parent_path):
with open(parent_path) as fd:
return fd.readlines()[0].strip()
return None

def get_theme_chain(theme, _themes_dir='themes'):
def get_theme_chain(theme, themes_dirs=['themes']):
"""Create the full theme inheritance chain."""
themes = [theme]

while True:
parent = get_parent_theme_name(themes[-1], _themes_dir)
parent = get_parent_theme_name(themes[-1], themes_dirs)
# Avoid silly loops
if parent is None or parent in themes:
@@ -635,7 +636,7 @@ def __str__(self):
return 'cannot find language {0}'.format(self.lang)

def load_messages(themes, translations, default_lang):
def load_messages(themes, translations, default_lang, themes_dirs=['themes']):
"""Load theme's messages into context.
All the messages from parent themes are loaded,
@@ -644,8 +645,8 @@ def load_messages(themes, translations, default_lang):
messages = Functionary(dict, default_lang)
oldpath = list(sys.path)
for theme_name in themes[::-1]:
msg_folder = os.path.join(get_theme_path(theme_name), 'messages')
default_folder = os.path.join(get_theme_path('base'), 'messages')
msg_folder = os.path.join(get_theme_path(theme_name, themes_dirs), 'messages')
default_folder = os.path.join(get_theme_path('base', themes_dirs), 'messages')
sys.path.insert(0, default_folder)
sys.path.insert(0, msg_folder)
english = __import__('messages_en')
@@ -978,7 +979,7 @@ def get_crumbs(path, is_file=False, index_folder=None, lang=None):
return list(reversed(_crumbs))

def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='themes', output_dir='output'):
def get_asset_path(path, themes, files_folders={'files': ''}, themes_dirs=['themes'], output_dir='output'):
"""Return the "real", absolute path to the asset.
By default, it checks which theme provides the asset.
@@ -1005,7 +1006,7 @@ def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='theme
for theme_name in themes:
candidate = os.path.join(
get_theme_path(theme_name, _themes_dir),
get_theme_path(theme_name, themes_dirs),
if os.path.isfile(candidate):

0 comments on commit 78c59a3

Please sign in to comment.