summaryrefslogtreecommitdiffstats
path: root/nikola/nikola.py
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/nikola.py')
-rw-r--r--nikola/nikola.py711
1 files changed, 453 insertions, 258 deletions
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 1d59954..59e1b97 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -28,6 +28,7 @@ from __future__ import print_function, unicode_literals
import codecs
from collections import defaultdict
from copy import copy
+from pkg_resources import resource_filename
import datetime
import glob
import locale
@@ -43,7 +44,7 @@ try:
import pyphen
except ImportError:
pyphen = None
-import pytz
+import dateutil.tz
import logging
from . import DEBUG
@@ -53,9 +54,18 @@ if DEBUG:
else:
logging.basicConfig(level=logging.ERROR)
+import PyRSS2Gen as rss
+
import lxml.html
from yapsy.PluginManager import PluginManager
+# Default "Read more..." link
+DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
+DEFAULT_RSS_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>'
+
+# Default pattern for translation files' names
+DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
+
from .post import Post
from . import utils
from .plugin_categories import (
@@ -63,20 +73,94 @@ from .plugin_categories import (
LateTask,
PageCompiler,
RestExtension,
+ MarkdownExtension,
Task,
TaskMultiplier,
TemplateSystem,
SignalHandler,
)
-from .utils import ColorfulStderrHandler
config_changed = utils.config_changed
__all__ = ['Nikola']
-# Default pattern for translation files' names
-DEFAULT_TRANSLATIONS_PATTERN = '{path}.{ext}.{lang}'
+# We store legal values for some setting here. For internal use.
+LEGAL_VALUES = {
+ 'COMMENT_SYSTEM': [
+ 'disqus',
+ 'facebook',
+ 'googleplus',
+ 'intensedebate',
+ 'isso',
+ 'livefyre',
+ 'muut',
+ ],
+ 'TRANSLATIONS': {
+ 'bg': 'Bulgarian',
+ 'ca': 'Catalan',
+ ('cs', 'cz'): 'Czech',
+ 'de': 'German',
+ ('el', '!gr'): 'Greek',
+ 'en': 'English',
+ 'eo': 'Esperanto',
+ 'es': 'Spanish',
+ 'et': 'Estonian',
+ 'eu': 'Basque',
+ 'fa': 'Persian',
+ 'fi': 'Finnish',
+ 'fr': 'French',
+ 'hi': 'Hindi',
+ 'hr': 'Croatian',
+ 'it': 'Italian',
+ ('ja', '!jp'): 'Japanese',
+ 'nb': 'Norwegian Bokmål',
+ 'nl': 'Dutch',
+ 'pl': 'Polish',
+ 'pt_br': 'Portuguese (Brasil)',
+ 'ru': 'Russian',
+ 'sk': 'Slovak',
+ 'sl': 'Slovene',
+ ('tr', '!tr_TR'): 'Turkish',
+ 'ur': 'Urdu',
+ 'zh_cn': 'Chinese (Simplified)',
+ },
+ '_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS': {
+ # This dict is used in `init` in case of locales that exist with a
+ # country specifier. If there is no other locale that has the same
+ # language with a different country, ``nikola init`` (but nobody else!)
+ # will accept it, warning the user about it.
+ 'pt': 'pt_br',
+ 'zh': 'zh_cn'
+ },
+ 'RTL_LANGUAGES': ('fa', 'ur'),
+ 'COLORBOX_LOCALES': defaultdict(
+ str,
+ bg='bg',
+ ca='ca',
+ cs='cs',
+ cz='cs',
+ de='de',
+ en='',
+ es='es',
+ et='et',
+ fa='fa',
+ fi='fi',
+ fr='fr',
+ hr='hr',
+ it='it',
+ ja='ja',
+ nb='no',
+ nl='nl',
+ pt_br='pt-br',
+ pl='pl',
+ ru='ru',
+ sk='sk',
+ sl='si', # country code is si, language code is sl, colorbox is wrong
+ tr='tr',
+ zh_cn='zh-CN'
+ )
+}
class Nikola(object):
@@ -85,12 +169,6 @@ class Nikola(object):
Takes a site config as argument on creation.
"""
- EXTRA_PLUGINS = [
- 'planetoid',
- 'ipynb',
- 'local_search',
- 'render_mustache',
- ]
def __init__(self, **config):
"""Setup proper environment for running tasks."""
@@ -117,24 +195,29 @@ class Nikola(object):
self._THEMES = None
self.debug = DEBUG
self.loghandlers = []
- if not config:
- self.configured = False
- self.colorful = False
- else:
- self.configured = True
- self.colorful = config.pop('__colorful__', False)
-
- ColorfulStderrHandler._colorful = self.colorful
+ self.colorful = config.pop('__colorful__', False)
+ self.invariant = config.pop('__invariant__', False)
+ self.quiet = config.pop('__quiet__', False)
+ self.configured = bool(config)
+
+ self.template_hooks = {
+ 'extra_head': utils.TemplateHookRegistry('extra_head', self),
+ 'body_end': utils.TemplateHookRegistry('body_end', self),
+ 'page_header': utils.TemplateHookRegistry('page_header', self),
+ 'menu': utils.TemplateHookRegistry('menu', self),
+ 'menu_alt': utils.TemplateHookRegistry('menu_alt', self),
+ 'page_footer': utils.TemplateHookRegistry('page_footer', self),
+ }
# Maintain API
utils.generic_rss_renderer = self.generic_rss_renderer
# This is the default config
self.config = {
- 'ADD_THIS_BUTTONS': True,
'ANNOTATIONS': False,
'ARCHIVE_PATH': "",
'ARCHIVE_FILENAME': "archive.html",
+ 'BLOG_AUTHOR': 'Default Author',
'BLOG_TITLE': 'Default Title',
'BLOG_DESCRIPTION': 'Default Description',
'BODY_END': "",
@@ -154,16 +237,16 @@ class Nikola(object):
"html": ('.html', '.htm')
},
'CONTENT_FOOTER': '',
+ 'CONTENT_FOOTER_FORMATS': {},
'COPY_SOURCES': True,
'CREATE_MONTHLY_ARCHIVE': False,
'CREATE_SINGLE_ARCHIVE': False,
'DATE_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': [],
- 'DISABLED_PLUGINS': (),
+ 'DISABLED_PLUGINS': [],
'EXTRA_PLUGINS_DIRS': [],
'COMMENT_SYSTEM_ID': 'nikolademo',
- 'ENABLED_EXTRAS': (),
'EXTRA_HEAD_DATA': '',
'FAVICONS': {},
'FEED_LENGTH': 10,
@@ -171,13 +254,12 @@ class Nikola(object):
'ADDITIONAL_METADATA': {},
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
+ 'FORCE_ISO8601': False,
'GALLERY_PATH': 'galleries',
'GALLERY_SORT_BY_DATE': True,
'GZIP_COMMAND': None,
'GZIP_FILES': False,
'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'),
- 'HIDE_SOURCELINK': False,
- 'HIDE_UNTRANSLATED_POSTS': False,
'HYPHENATE': False,
'INDEX_DISPLAY_POST_COUNT': 10,
'INDEX_FILE': 'index.html',
@@ -192,7 +274,8 @@ class Nikola(object):
'LICENSE': '',
'LINK_CHECK_WHITELIST': [],
'LISTINGS_FOLDER': 'listings',
- 'NAVIGATION_LINKS': None,
+ 'LOGO_URL': '',
+ 'NAVIGATION_LINKS': {},
'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'],
'MAX_IMAGE_SIZE': 1280,
'MATHJAX_CONFIG': '',
@@ -202,14 +285,21 @@ class Nikola(object):
'PAGES': (("stories/*.txt", "stories", "story.tmpl"),),
'PRETTY_URLS': False,
'FUTURE_IS_NOW': False,
- 'READ_MORE_LINK': '<p class="more"><a href="{link}">{read_more}…</a></p>',
+ 'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK,
+ 'RSS_READ_MORE_LINK': DEFAULT_RSS_READ_MORE_LINK,
'REDIRECTIONS': [],
+ 'ROBOTS_EXCLUSIONS': [],
+ 'GENERATE_RSS': True,
'RSS_LINK': None,
'RSS_PATH': '',
+ 'RSS_PLAIN': False,
'RSS_TEASERS': True,
'SASS_COMPILER': 'sass',
'SASS_OPTIONS': [],
'SEARCH_FORM': '',
+ 'SHOW_BLOG_TITLE': True,
+ 'SHOW_SOURCELINK': True,
+ 'SHOW_UNTRANSLATED_POSTS': True,
'SLUG_TAG_PATH': True,
'SOCIAL_BUTTONS_CODE': SOCIAL_BUTTONS_CODE,
'SITE_URL': 'http://getnikola.com/',
@@ -223,23 +313,82 @@ class Nikola(object):
'THEME_REVEAL_CONFIG_SUBTHEME': 'sky',
'THEME_REVEAL_CONFIG_TRANSITION': 'cube',
'THUMBNAIL_SIZE': 180,
+ 'UNSLUGIFY_TITLES': False, # WARNING: conf.py.in overrides this with True for backwards compatibility
'URL_TYPE': 'rel_path',
'USE_BUNDLES': True,
'USE_CDN': False,
'USE_FILENAME_AS_TITLE': True,
+ 'USE_OPEN_GRAPH': True,
'TIMEZONE': 'UTC',
'DEPLOY_DRAFTS': True,
'DEPLOY_FUTURE': False,
'SCHEDULE_ALL': False,
'SCHEDULE_RULE': '',
- 'SCHEDULE_FORCE_TODAY': False,
'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}},
'DEMOTE_HEADERS': 1,
- 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN,
}
+ # set global_context for template rendering
+ self._GLOBAL_CONTEXT = {}
+
self.config.update(config)
+ # __builtins__ contains useless cruft
+ if '__builtins__' in self.config:
+ try:
+ del self.config['__builtins__']
+ except KeyError:
+ del self.config[b'__builtins__']
+
+ self.config['__colorful__'] = self.colorful
+ self.config['__invariant__'] = self.invariant
+ self.config['__quiet__'] = self.quiet
+
+ # Make sure we have sane NAVIGATION_LINKS.
+ if not self.config['NAVIGATION_LINKS']:
+ self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()}
+
+ # Translatability configuration.
+ self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
+ {self.config['DEFAULT_LANG']: ''})
+ utils.TranslatableSetting.default_lang = self.config['DEFAULT_LANG']
+
+ self.TRANSLATABLE_SETTINGS = ('BLOG_AUTHOR',
+ 'BLOG_TITLE',
+ 'BLOG_DESCRIPTION',
+ 'LICENSE',
+ 'CONTENT_FOOTER',
+ 'SOCIAL_BUTTONS_CODE',
+ 'SEARCH_FORM',
+ 'BODY_END',
+ 'EXTRA_HEAD_DATA',
+ 'NAVIGATION_LINKS',
+ 'INDEX_READ_MORE_LINK',
+ 'RSS_READ_MORE_LINK',)
+
+ self._GLOBAL_CONTEXT_TRANSLATABLE = ('blog_author',
+ 'blog_title',
+ 'blog_desc', # TODO: remove in v8
+ 'blog_description',
+ 'license',
+ 'content_footer',
+ 'social_buttons_code',
+ 'search_form',
+ 'body_end',
+ 'extra_head_data',)
+ # WARNING: navigation_links SHOULD NOT be added to the list above.
+ # Themes ask for [lang] there and we should provide it.
+
+ for i in self.TRANSLATABLE_SETTINGS:
+ try:
+ self.config[i] = utils.TranslatableSetting(i, self.config[i], self.config['TRANSLATIONS'])
+ except KeyError:
+ pass
+
+ # Handle CONTENT_FOOTER properly.
+ # We provide the arguments to format in CONTENT_FOOTER_FORMATS.
+ self.config['CONTENT_FOOTER'].langformat(self.config['CONTENT_FOOTER_FORMATS'])
+
# Make sure we have pyphen installed if we are using it
if self.config.get('HYPHENATE') and pyphen is None:
utils.LOGGER.warn('To use the hyphenation, you have to install '
@@ -247,24 +396,6 @@ class Nikola(object):
utils.LOGGER.warn('Setting HYPHENATE to False.')
self.config['HYPHENATE'] = False
- # Deprecating post_compilers
- # TODO: remove on v7
- if 'post_compilers' in config:
- utils.LOGGER.warn('The post_compilers option is deprecated, use COMPILERS instead.')
- if 'COMPILERS' in config:
- utils.LOGGER.warn('COMPILERS conflicts with post_compilers, ignoring post_compilers.')
- else:
- self.config['COMPILERS'] = config['post_compilers']
-
- # Deprecating post_pages
- # TODO: remove on v7
- if 'post_pages' in config:
- utils.LOGGER.warn('The post_pages option is deprecated, use POSTS and PAGES instead.')
- if 'POSTS' in config or 'PAGES' in config:
- utils.LOGGER.warn('POSTS and PAGES conflict with post_pages, ignoring post_pages.')
- else:
- self.config['POSTS'] = [item[:3] for item in config['post_pages'] if item[-1]]
- self.config['PAGES'] = [item[:3] for item in config['post_pages'] if not item[-1]]
# FIXME: Internally, we still use post_pages because it's a pain to change it
self.config['post_pages'] = []
for i1, i2, i3 in self.config['POSTS']:
@@ -272,80 +403,78 @@ class Nikola(object):
for i1, i2, i3 in self.config['PAGES']:
self.config['post_pages'].append([i1, i2, i3, False])
- # Deprecating DISQUS_FORUM
- # TODO: remove on v7
- if 'DISQUS_FORUM' in config:
- utils.LOGGER.warn('The DISQUS_FORUM option is deprecated, use COMMENT_SYSTEM_ID instead.')
- if 'COMMENT_SYSTEM_ID' in config:
- utils.LOGGER.warn('DISQUS_FORUM conflicts with COMMENT_SYSTEM_ID, ignoring DISQUS_FORUM.')
+ # DEFAULT_TRANSLATIONS_PATTERN was changed from "p.e.l" to "p.l.e"
+ # TODO: remove on v8
+ if 'TRANSLATIONS_PATTERN' not in self.config:
+ if len(self.config.get('TRANSLATIONS', {})) > 1:
+ utils.LOGGER.warn('You do not have a TRANSLATIONS_PATTERN set in your config, yet you have multiple languages.')
+ utils.LOGGER.warn('Setting TRANSLATIONS_PATTERN to the pre-v6 default ("{path}.{ext}.{lang}").')
+ utils.LOGGER.warn('Please add the proper pattern to your conf.py. (The new default in v7 is "{0}".)'.format(DEFAULT_TRANSLATIONS_PATTERN))
+ self.config['TRANSLATIONS_PATTERN'] = "{path}.{ext}.{lang}"
else:
- self.config['COMMENT_SYSTEM_ID'] = config['DISQUS_FORUM']
-
- # Deprecating the ANALYTICS option
- # TODO: remove on v7
- if 'ANALYTICS' in config:
- utils.LOGGER.warn('The ANALYTICS option is deprecated, use BODY_END instead.')
- if 'BODY_END' in config:
- utils.LOGGER.warn('ANALYTICS conflicts with BODY_END, ignoring ANALYTICS.')
+ # use v7 default there
+ self.config['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN
+
+ # HIDE_SOURCELINK has been replaced with the inverted SHOW_SOURCELINK
+ # TODO: remove on v8
+ if 'HIDE_SOURCELINK' in config:
+ utils.LOGGER.warn('The HIDE_SOURCELINK option is deprecated, use SHOW_SOURCELINK instead.')
+ if 'SHOW_SOURCELINK' in config:
+ utils.LOGGER.warn('HIDE_SOURCELINK conflicts with SHOW_SOURCELINK, ignoring HIDE_SOURCELINK.')
+ self.config['SHOW_SOURCELINK'] = not config['HIDE_SOURCELINK']
+
+ # HIDE_UNTRANSLATED_POSTS has been replaced with the inverted SHOW_UNTRANSLATED_POSTS
+ # TODO: remove on v8
+ if 'HIDE_UNTRANSLATED_POSTS' in config:
+ utils.LOGGER.warn('The HIDE_UNTRANSLATED_POSTS option is deprecated, use SHOW_UNTRANSLATED_POSTS instead.')
+ if 'SHOW_UNTRANSLATED_POSTS' in config:
+ utils.LOGGER.warn('HIDE_UNTRANSLATED_POSTS conflicts with SHOW_UNTRANSLATED_POSTS, ignoring HIDE_UNTRANSLATED_POSTS.')
+ self.config['SHOW_UNTRANSLATED_POSTS'] = not config['HIDE_UNTRANSLATED_POSTS']
+
+ # READ_MORE_LINK has been split into INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK
+ # TODO: remove on v8
+ if 'READ_MORE_LINK' in config:
+ utils.LOGGER.warn('The READ_MORE_LINK option is deprecated, use INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK instead.')
+ if 'INDEX_READ_MORE_LINK' in config:
+ utils.LOGGER.warn('READ_MORE_LINK conflicts with INDEX_READ_MORE_LINK, ignoring READ_MORE_LINK.')
else:
- self.config['BODY_END'] = config['ANALYTICS']
-
- # Deprecating the SIDEBAR_LINKS option
- # TODO: remove on v7
- if 'SIDEBAR_LINKS' in config:
- utils.LOGGER.warn('The SIDEBAR_LINKS option is deprecated, use NAVIGATION_LINKS instead.')
- if 'NAVIGATION_LINKS' in config:
- utils.LOGGER.warn('The SIDEBAR_LINKS conflicts with NAVIGATION_LINKS, ignoring SIDEBAR_LINKS.')
+ self.config['INDEX_READ_MORE_LINK'] = utils.TranslatableSetting('INDEX_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS'])
+
+ if 'RSS_READ_MORE_LINK' in config:
+ utils.LOGGER.warn('READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring READ_MORE_LINK.')
else:
- self.config['NAVIGATION_LINKS'] = config['SIDEBAR_LINKS']
- # Compatibility alias
- self.config['SIDEBAR_LINKS'] = self.config['NAVIGATION_LINKS']
+ self.config['RSS_READ_MORE_LINK'] = utils.TranslatableSetting('RSS_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS'])
- if self.config['NAVIGATION_LINKS'] in (None, {}):
- self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()}
+ # Moot.it renamed themselves to muut.io
+ # TODO: remove on v8?
+ if self.config.get('COMMENT_SYSTEM') == 'moot':
+ utils.LOGGER.warn('The moot comment system has been renamed to muut by the upstream. Setting COMMENT_SYSTEM to "muut".')
+ self.config['COMMENT_SYSTEM'] = 'muut'
+
+ # Disable RSS. For a successful disable, we must have both the option
+ # false and the plugin disabled through the official means.
+ if 'generate_rss' in self.config['DISABLED_PLUGINS'] and self.config['GENERATE_RSS'] is True:
+ self.config['GENERATE_RSS'] = False
- # Deprecating the ADD_THIS_BUTTONS option
- # TODO: remove on v7
- if 'ADD_THIS_BUTTONS' in config:
- utils.LOGGER.warn('The ADD_THIS_BUTTONS option is deprecated, use SOCIAL_BUTTONS_CODE instead.')
- if not config['ADD_THIS_BUTTONS']:
- utils.LOGGER.warn('Setting SOCIAL_BUTTONS_CODE to empty because ADD_THIS_BUTTONS is False.')
- self.config['SOCIAL_BUTTONS_CODE'] = ''
-
- # STRIP_INDEX_HTML config has been replaces with STRIP_INDEXES
- # Port it if only the oldef form is there
- # TODO: remove on v7
- if 'STRIP_INDEX_HTML' in config and 'STRIP_INDEXES' not in config:
- utils.LOGGER.warn('You should configure STRIP_INDEXES instead of STRIP_INDEX_HTML')
- self.config['STRIP_INDEXES'] = config['STRIP_INDEX_HTML']
+ if not self.config['GENERATE_RSS'] and 'generate_rss' not in self.config['DISABLED_PLUGINS']:
+ self.config['DISABLED_PLUGINS'].append('generate_rss')
# PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled
if self.config.get('PRETTY_URLS') and 'STRIP_INDEXES' not in config:
self.config['STRIP_INDEXES'] = True
if not self.config.get('COPY_SOURCES'):
- self.config['HIDE_SOURCELINK'] = True
-
- self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
- {self.config['DEFAULT_LANG']: ''})
-
- # SITE_URL is required, but if the deprecated BLOG_URL
- # is available, use it and warn
- # TODO: remove on v7
- if 'SITE_URL' not in self.config:
- if 'BLOG_URL' in self.config:
- utils.LOGGER.warn('You should configure SITE_URL instead of BLOG_URL')
- self.config['SITE_URL'] = self.config['BLOG_URL']
+ self.config['SHOW_SOURCELINK'] = False
self.default_lang = self.config['DEFAULT_LANG']
self.translations = self.config['TRANSLATIONS']
- locale_fallback, locale_default, locales = sanitized_locales(
- self.config.get('LOCALE_FALLBACK', None),
- self.config.get('LOCALE_DEFAULT', None),
- self.config.get('LOCALES', {}),
- self.translations) # NOQA
- utils.LocaleBorg.initialize(locales, self.default_lang)
+ if self.configured:
+ locale_fallback, locale_default, locales = sanitized_locales(
+ self.config.get('LOCALE_FALLBACK', None),
+ self.config.get('LOCALE_DEFAULT', None),
+ self.config.get('LOCALES', {}), self.translations)
+ utils.LocaleBorg.initialize(locales, self.default_lang)
# BASE_URL defaults to SITE_URL
if 'BASE_URL' not in self.config:
@@ -354,6 +483,10 @@ class Nikola(object):
if self.config['BASE_URL'] and self.config['BASE_URL'][-1] != '/':
utils.LOGGER.warn("Your BASE_URL doesn't end in / -- adding it.")
+ # We use one global tzinfo object all over Nikola.
+ self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])
+ self.config['__tzinfo__'] = self.tzinfo
+
self.plugin_manager = PluginManager(categories_filter={
"Command": Command,
"Task": Task,
@@ -362,19 +495,22 @@ class Nikola(object):
"PageCompiler": PageCompiler,
"TaskMultiplier": TaskMultiplier,
"RestExtension": RestExtension,
+ "MarkdownExtension": MarkdownExtension,
"SignalHandler": SignalHandler,
})
self.plugin_manager.setPluginInfoExtension('plugin')
extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS']
if sys.version_info[0] == 3:
places = [
- os.path.join(os.path.dirname(__file__), 'plugins'),
+ resource_filename('nikola', 'plugins'),
os.path.join(os.getcwd(), 'plugins'),
+ os.path.expanduser('~/.nikola/plugins'),
] + [path for path in extra_plugins_dirs if path]
else:
places = [
- os.path.join(os.path.dirname(__file__), utils.sys_encode('plugins')),
+ resource_filename('nikola', utils.sys_encode('plugins')),
os.path.join(os.getcwd(), utils.sys_encode('plugins')),
+ os.path.expanduser('~/.nikola/plugins'),
] + [utils.sys_encode(path) for path in extra_plugins_dirs if path]
self.plugin_manager.setPluginPlaces(places)
@@ -391,26 +527,22 @@ class Nikola(object):
# Emit signal for SignalHandlers which need to start running immediately.
signal('sighandlers_loaded').send(self)
- self.commands = {}
+ self._commands = {}
# Activate all command plugins
for plugin_info in self.plugin_manager.getPluginsOfCategory("Command"):
- if (plugin_info.name in self.config['DISABLED_PLUGINS']
- or (plugin_info.name in self.EXTRA_PLUGINS and
- plugin_info.name not in self.config['ENABLED_EXTRAS'])):
+ if plugin_info.name in self.config['DISABLED_PLUGINS']:
self.plugin_manager.removePluginFromCategory(plugin_info, "Command")
continue
self.plugin_manager.activatePluginByName(plugin_info.name)
plugin_info.plugin_object.set_site(self)
plugin_info.plugin_object.short_help = plugin_info.description
- self.commands[plugin_info.name] = plugin_info.plugin_object
+ self._commands[plugin_info.name] = plugin_info.plugin_object
# Activate all task plugins
for task_type in ["Task", "LateTask"]:
for plugin_info in self.plugin_manager.getPluginsOfCategory(task_type):
- if (plugin_info.name in self.config['DISABLED_PLUGINS']
- or (plugin_info.name in self.EXTRA_PLUGINS and
- plugin_info.name not in self.config['ENABLED_EXTRAS'])):
+ if plugin_info.name in self.config['DISABLED_PLUGINS']:
self.plugin_manager.removePluginFromCategory(plugin_info, task_type)
continue
self.plugin_manager.activatePluginByName(plugin_info.name)
@@ -418,20 +550,24 @@ class Nikola(object):
# Activate all multiplier plugins
for plugin_info in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"):
- if (plugin_info.name in self.config['DISABLED_PLUGINS']
- or (plugin_info.name in self.EXTRA_PLUGINS and
- plugin_info.name not in self.config['ENABLED_EXTRAS'])):
+ if plugin_info.name in self.config['DISABLED_PLUGINS']:
self.plugin_manager.removePluginFromCategory(plugin_info, task_type)
continue
self.plugin_manager.activatePluginByName(plugin_info.name)
plugin_info.plugin_object.set_site(self)
+ compilers = defaultdict(set)
# Also add aliases for combinations with TRANSLATIONS_PATTERN
- self.config['COMPILERS'] = dict([(lang, list(exts) + [
- utils.get_translation_candidate(self.config, "f" + ext, lang)[1:]
- for ext in exts
- for lang in self.config['TRANSLATIONS'].keys()])
- for lang, exts in list(self.config['COMPILERS'].items())])
+ for compiler, exts in self.config['COMPILERS'].items():
+ for ext in exts:
+ compilers[compiler].add(ext)
+ for lang in self.config['TRANSLATIONS'].keys():
+ candidate = utils.get_translation_candidate(self.config, "f" + ext, lang)
+ compilers[compiler].add(candidate)
+
+ # Avoid redundant compilers
+ for k, v in compilers.items():
+ self.config['COMPILERS'][k] = sorted(list(v))
# Activate all required compiler plugins
for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"):
@@ -439,11 +575,13 @@ class Nikola(object):
self.plugin_manager.activatePluginByName(plugin_info.name)
plugin_info.plugin_object.set_site(self)
- # set global_context for template rendering
- self._GLOBAL_CONTEXT = {}
-
+ self._GLOBAL_CONTEXT['url_type'] = self.config['URL_TYPE']
+ self._GLOBAL_CONTEXT['timezone'] = self.tzinfo
self._GLOBAL_CONTEXT['_link'] = self.link
- self._GLOBAL_CONTEXT['set_locale'] = utils.LocaleBorg().set_locale
+ try:
+ self._GLOBAL_CONTEXT['set_locale'] = utils.LocaleBorg().set_locale
+ except utils.LocaleBorgUninitializedException:
+ self._GLOBAL_CONTEXT['set_locale'] = None
self._GLOBAL_CONTEXT['rel_link'] = self.rel_link
self._GLOBAL_CONTEXT['abs_link'] = self.abs_link
self._GLOBAL_CONTEXT['exists'] = self.file_exists
@@ -458,49 +596,45 @@ class Nikola(object):
'DATE_FORMAT', '%Y-%m-%d %H:%M')
self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
self._GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE')
+ self._GLOBAL_CONTEXT['show_blog_title'] = self.config.get('SHOW_BLOG_TITLE')
+ self._GLOBAL_CONTEXT['logo_url'] = self.config.get('LOGO_URL')
+ self._GLOBAL_CONTEXT['blog_description'] = self.config.get('BLOG_DESCRIPTION')
- # TODO: remove fallback in v7
- self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL'))
+ # TODO: remove in v8
self._GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
+
+ self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL')
+ self._GLOBAL_CONTEXT['template_hooks'] = self.template_hooks
self._GLOBAL_CONTEXT['body_end'] = self.config.get('BODY_END')
- # TODO: remove in v7
- self._GLOBAL_CONTEXT['analytics'] = self.config.get('BODY_END')
- # TODO: remove in v7
- self._GLOBAL_CONTEXT['add_this_buttons'] = self.config.get('SOCIAL_BUTTONS_CODE')
self._GLOBAL_CONTEXT['social_buttons_code'] = self.config.get('SOCIAL_BUTTONS_CODE')
self._GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS')
self._GLOBAL_CONTEXT['license'] = self.config.get('LICENSE')
self._GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM')
self._GLOBAL_CONTEXT['comment_system'] = self.config.get('COMMENT_SYSTEM')
self._GLOBAL_CONTEXT['comment_system_id'] = self.config.get('COMMENT_SYSTEM_ID')
- # TODO: remove in v7
- self._GLOBAL_CONTEXT['disqus_forum'] = self.config.get('COMMENT_SYSTEM_ID')
+ self._GLOBAL_CONTEXT['site_has_comments'] = bool(self.config.get('COMMENT_SYSTEM'))
self._GLOBAL_CONTEXT['mathjax_config'] = self.config.get(
'MATHJAX_CONFIG')
self._GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONFIG_SUBTHEME')
self._GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONFIG_TRANSITION')
self._GLOBAL_CONTEXT['content_footer'] = self.config.get(
'CONTENT_FOOTER')
+ self._GLOBAL_CONTEXT['generate_rss'] = self.config.get('GENERATE_RSS')
self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
- self._GLOBAL_CONTEXT['navigation_links'] = utils.Functionary(list, self.config['DEFAULT_LANG'])
- for k, v in self.config.get('NAVIGATION_LINKS', {}).items():
- self._GLOBAL_CONTEXT['navigation_links'][k] = v
-
- # avoid #1082 by making sure all keys in navigation_links are read once
- for k in self._GLOBAL_CONTEXT['translations']:
- self._GLOBAL_CONTEXT['navigation_links'][k]
-
- # TODO: remove on v7
- # Compatibility alias
- self._GLOBAL_CONTEXT['sidebar_links'] = self._GLOBAL_CONTEXT['navigation_links']
+ self._GLOBAL_CONTEXT['navigation_links'] = self.config.get('NAVIGATION_LINKS')
+ self._GLOBAL_CONTEXT['use_open_graph'] = self.config.get(
+ 'USE_OPEN_GRAPH', True)
self._GLOBAL_CONTEXT['twitter_card'] = self.config.get(
'TWITTER_CARD', {})
- self._GLOBAL_CONTEXT['hide_sourcelink'] = self.config.get(
- 'HIDE_SOURCELINK')
+ self._GLOBAL_CONTEXT['hide_sourcelink'] = not self.config.get(
+ 'SHOW_SOURCELINK')
+ self._GLOBAL_CONTEXT['show_sourcelink'] = self.config.get(
+ 'SHOW_SOURCELINK')
self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
+ self._GLOBAL_CONTEXT['colorbox_locales'] = LEGAL_VALUES['COLORBOX_LOCALES']
self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
@@ -512,29 +646,15 @@ class Nikola(object):
"PageCompiler"):
self.compilers[plugin_info.name] = \
plugin_info.plugin_object
+
signal('configured').send(self)
def _get_themes(self):
if self._THEMES is None:
- # Check for old theme names (Issue #650) TODO: remove in v7
- theme_replacements = {
- 'site': 'bootstrap',
- 'orphan': 'base',
- 'default': 'oldfashioned',
- }
- if self.config['THEME'] in theme_replacements:
- utils.LOGGER.warn('You are using the old theme "{0}", using "{1}" instead.'.format(
- self.config['THEME'], theme_replacements[self.config['THEME']]))
- self.config['THEME'] = theme_replacements[self.config['THEME']]
- if self.config['THEME'] == 'oldfashioned':
- utils.LOGGER.warn('''You may need to install the "oldfashioned" theme '''
- '''from themes.getnikola.com because it's not '''
- '''shipped by default anymore.''')
- utils.LOGGER.warn('Please change your THEME setting.')
try:
self._THEMES = utils.get_theme_chain(self.config['THEME'])
except Exception:
- utils.LOGGER.warn('''Can't load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME']))
+ utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME']))
self.config['THEME'] = 'bootstrap'
return self._get_themes()
# Check consistency of USE_CDN and the current THEME (Issue #386)
@@ -549,9 +669,13 @@ class Nikola(object):
THEMES = property(_get_themes)
def _get_messages(self):
- return utils.load_messages(self.THEMES,
- self.translations,
- self.default_lang)
+ try:
+ return utils.load_messages(self.THEMES,
+ self.translations,
+ self.default_lang)
+ 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))
+ sys.exit(1)
MESSAGES = property(_get_messages)
@@ -633,8 +757,14 @@ class Nikola(object):
local_context["template_name"] = template_name
local_context.update(self.GLOBAL_CONTEXT)
local_context.update(context)
+ for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
+ local_context[k] = local_context[k](local_context['lang'])
+ local_context['is_rtl'] = local_context['lang'] in LEGAL_VALUES['RTL_LANGUAGES']
# string, arguments
local_context["formatmsg"] = lambda s, *a: s % a
+ for h in local_context['template_hooks'].values():
+ h.context = context
+
data = self.template_system.render_template(
template_name, None, local_context)
@@ -652,7 +782,7 @@ class Nikola(object):
utils.makedirs(os.path.dirname(output_name))
doc = lxml.html.document_fromstring(data)
doc.rewrite_links(lambda dst: self.url_replacer(src, dst, context['lang']))
- data = b'<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8')
+ data = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True)
with open(output_name, "wb+") as post_file:
post_file.write(data)
@@ -745,30 +875,44 @@ class Nikola(object):
return result
def generic_rss_renderer(self, lang, title, link, description, timeline, output_path,
- rss_teasers, feed_length=10, feed_url=None):
+ rss_teasers, rss_plain, feed_length=10, feed_url=None, enclosure=None):
+
"""Takes all necessary data, and renders a RSS feed in output_path."""
+ rss_obj = rss.RSS2(
+ title=title,
+ link=link,
+ description=description,
+ lastBuildDate=datetime.datetime.now(),
+ generator='http://getnikola.com/',
+ language=lang
+ )
+
items = []
+
for post in timeline[:feed_length]:
- # Massage the post's HTML
- data = post.text(lang, teaser_only=rss_teasers, really_absolute=True)
+ old_url_type = self.config['URL_TYPE']
+ self.config['URL_TYPE'] = 'absolute'
+ data = post.text(lang, teaser_only=rss_teasers, strip_html=rss_plain, rss_read_more_link=True)
if feed_url is not None and data:
- # FIXME: this is duplicated with code in Post.text()
- try:
- doc = lxml.html.document_fromstring(data)
- doc.rewrite_links(lambda dst: self.url_replacer(feed_url, dst, lang))
+ # Massage the post's HTML (unless plain)
+ if not rss_plain:
+ # FIXME: this is duplicated with code in Post.text()
try:
- body = doc.body
- data = (body.text or '') + ''.join(
- [lxml.html.tostring(child, encoding='unicode')
- for child in body.iterchildren()])
- except IndexError: # No body there, it happens sometimes
- data = ''
- except lxml.etree.ParserError as e:
- if str(e) == "Document is empty":
- data = ""
- else: # let other errors raise
- raise(e)
-
+ doc = lxml.html.document_fromstring(data)
+ doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang))
+ try:
+ body = doc.body
+ data = (body.text or '') + ''.join(
+ [lxml.html.tostring(child, encoding='unicode')
+ for child in body.iterchildren()])
+ except IndexError: # No body there, it happens sometimes
+ data = ''
+ except lxml.etree.ParserError as e:
+ if str(e) == "Document is empty":
+ data = ""
+ else: # let other errors raise
+ raise(e)
+ self.config['URL_TYPE'] = old_url_type
args = {
'title': post.title(lang),
'link': post.permalink(lang, absolute=True),
@@ -776,24 +920,28 @@ class Nikola(object):
'guid': post.permalink(lang, absolute=True),
# PyRSS2Gen's pubDate is GMT time.
'pubDate': (post.date if post.date.tzinfo is None else
- post.date.astimezone(pytz.timezone('UTC'))),
+ post.date.astimezone(dateutil.tz.tzutc())),
'categories': post._tags.get(lang, []),
- 'author': post.meta('author'),
+ 'creator': post.author(lang),
}
+ if post.author(lang):
+ rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
+
+ """ Enclosure callback must returns tuple """
+ if enclosure:
+ download_link, download_size, download_type = enclosure(post=post, lang=lang)
+
+ args['enclosure'] = rss.Enclosure(
+ download_link,
+ download_size,
+ download_type,
+ )
+
items.append(utils.ExtendedItem(**args))
- rss_obj = utils.ExtendedRSS2(
- title=title,
- link=link,
- description=description,
- lastBuildDate=datetime.datetime.now(),
- items=items,
- generator='Nikola <http://getnikola.com/>',
- language=lang
- )
- rss_obj.self_url = feed_url
- rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
- rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
+
+ rss_obj.items = items
+
dst_dir = os.path.dirname(output_path)
utils.makedirs(dst_dir)
with codecs.open(output_path, "wb+", "utf-8") as rss_file:
@@ -838,19 +986,23 @@ class Nikola(object):
if lang is None:
lang = utils.LocaleBorg().current_lang
- 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'])
- if self.config['STRIP_INDEXES'] and \
- link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']:
- return link[:-index_len]
+ 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'])
+ if self.config['STRIP_INDEXES'] and \
+ link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']:
+ return link[:-index_len]
+ else:
+ return link
else:
- return link
- else:
- return os.path.join(*path)
+ return os.path.join(*path)
+ except KeyError:
+ utils.LOGGER.warn("Unknown path request of kind: {0}".format(kind))
+ return ""
def post_path(self, name, lang):
"""post_path path handler"""
@@ -862,7 +1014,7 @@ class Nikola(object):
"""slug path handler"""
results = [p for p in self.timeline if p.meta('slug') == name]
if not results:
- utils.LOGGER.warning("Can't resolve path request for slug: {0}".format(name))
+ utils.LOGGER.warning("Cannot resolve path request for slug: {0}".format(name))
else:
if len(results) > 1:
utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name))
@@ -872,7 +1024,7 @@ class Nikola(object):
"""filename path handler"""
results = [p for p in self.timeline if p.source_path == name]
if not results:
- utils.LOGGER.warning("Can't resolve path request for filename: {0}".format(name))
+ utils.LOGGER.warning("Cannot resolve path request for filename: {0}".format(name))
else:
if len(results) > 1:
utils.LOGGER.error("Ambiguous path request for filename: {0}".format(name))
@@ -887,13 +1039,16 @@ class Nikola(object):
def link(self, *args):
return self.path(*args, is_link=True)
- def abs_link(self, dst):
+ def abs_link(self, dst, protocol_relative=False):
# Normalize
if dst: # Mako templates and empty strings evaluate to False
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
else:
dst = self.config['BASE_URL']
- return urlparse(dst).geturl()
+ url = urlparse(dst).geturl()
+ if protocol_relative:
+ url = url.split(":", 1)[1]
+ return url
def rel_link(self, src, dst):
# Normalize
@@ -967,40 +1122,55 @@ class Nikola(object):
'task_dep': task_dep
}
- def scan_posts(self):
+ def scan_posts(self, really=False):
"""Scan all the posts."""
- if self._scanned:
+ if self._scanned and not really:
return
+
+ self.commands = utils.Commands(self.doit)
+ self.global_data = {}
+ self.posts = []
+ self.posts_per_year = defaultdict(list)
+ self.posts_per_month = defaultdict(list)
+ self.posts_per_tag = defaultdict(list)
+ self.posts_per_category = defaultdict(list)
+ self.post_per_file = {}
+ self.timeline = []
+ self.pages = []
+
seen = set([])
- print("Scanning posts", end='', file=sys.stderr)
+ if not self.quiet:
+ print("Scanning posts", end='', file=sys.stderr)
slugged_tags = set([])
quit = False
for wildcard, destination, template_name, use_in_feeds in \
self.config['post_pages']:
- print(".", end='', file=sys.stderr)
+ if not self.quiet:
+ print(".", end='', file=sys.stderr)
dirname = os.path.dirname(wildcard)
- for dirpath, _, _ in os.walk(dirname):
- dir_glob = os.path.join(dirpath, os.path.basename(wildcard))
+ for dirpath, _, _ in os.walk(dirname, followlinks=True):
dest_dir = os.path.normpath(os.path.join(destination,
- os.path.relpath(dirpath, dirname)))
- full_list = glob.glob(dir_glob)
- # Now let's look for things that are not in default_lang
+ os.path.relpath(dirpath, dirname))) # output/destination/foo/
+ # Get all the untranslated paths
+ dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst
+ untranslated = glob.glob(dir_glob)
+ # And now get all the translated paths
+ translated = set([])
for lang in self.config['TRANSLATIONS'].keys():
- lang_glob = utils.get_translation_candidate(self.config, dir_glob, lang)
- translated_list = glob.glob(lang_glob)
- # dir_glob could have put it already in full_list
- full_list = list(set(full_list + translated_list))
-
- # Eliminate translations from full_list if they are not the primary,
- # or a secondary with no primary
- limited_list = full_list[:]
- for fname in full_list:
- for lang in self.config['TRANSLATIONS'].keys():
- translation = utils.get_translation_candidate(self.config, fname, lang)
- if translation in full_list:
- limited_list.remove(translation)
- full_list = limited_list
-
+ if lang == self.config['DEFAULT_LANG']:
+ continue
+ lang_glob = utils.get_translation_candidate(self.config, dir_glob, lang) # posts/foo/*.LANG.rst
+ translated = translated.union(set(glob.glob(lang_glob)))
+ # untranslated globs like *.rst often match translated paths too, so remove them
+ # and ensure x.rst is not in the translated set
+ untranslated = set(untranslated) - translated
+
+ # also remove from translated paths that are translations of
+ # paths in untranslated_list, so x.es.rst is not in the untranslated set
+ for p in untranslated:
+ translated = translated - set([utils.get_translation_candidate(self.config, p, l) for l in self.config['TRANSLATIONS'].keys()])
+
+ full_list = list(translated) + list(untranslated)
# We eliminate from the list the files inside any .ipynb folder
full_list = [p for p in full_list
if not any([x.startswith('.')
@@ -1020,13 +1190,14 @@ class Nikola(object):
template_name,
self.get_compiler(base_path)
)
+ self.timeline.append(post)
self.global_data[post.source_path] = post
if post.use_in_feeds:
- self.posts.append(post.source_path)
+ self.posts.append(post)
self.posts_per_year[
- str(post.date.year)].append(post.source_path)
+ str(post.date.year)].append(post)
self.posts_per_month[
- '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.source_path)
+ '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post)
for tag in post.alltags:
if utils.slugify(tag) in slugged_tags:
if tag not in self.posts_per_tag:
@@ -1034,28 +1205,35 @@ class Nikola(object):
other_tag = [k for k in self.posts_per_tag.keys() if k.lower() == tag.lower()][0]
utils.LOGGER.error('You have tags that are too similar: {0} and {1}'.format(tag, other_tag))
utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path))
- utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join(self.posts_per_tag[other_tag])))
+ utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join([p.source_path for p in self.posts_per_tag[other_tag]])))
quit = True
else:
slugged_tags.add(utils.slugify(tag))
- self.posts_per_tag[tag].append(post.source_path)
- self.posts_per_category[post.meta('category')].append(post.source_path)
+ self.posts_per_tag[tag].append(post)
+ self.posts_per_category[post.meta('category')].append(post)
else:
self.pages.append(post)
self.post_per_file[post.destination_path(lang=lang)] = post
self.post_per_file[post.destination_path(lang=lang, extension=post.source_ext())] = post
- for name, post in list(self.global_data.items()):
- self.timeline.append(post)
+ # Sort everything.
self.timeline.sort(key=lambda p: p.date)
self.timeline.reverse()
- post_timeline = [p for p in self.timeline if p.use_in_feeds]
- for i, p in enumerate(post_timeline[1:]):
- p.next_post = post_timeline[i]
- for i, p in enumerate(post_timeline[:-1]):
- p.prev_post = post_timeline[i + 1]
+ self.posts.sort(key=lambda p: p.date)
+ self.posts.reverse()
+ self.pages.sort(key=lambda p: p.date)
+ self.pages.reverse()
+
+ for i, p in enumerate(self.posts[1:]):
+ p.next_post = self.posts[i]
+ for i, p in enumerate(self.posts[:-1]):
+ p.prev_post = self.posts[i + 1]
self._scanned = True
- print("done!", file=sys.stderr)
+ if not self.quiet:
+ print("done!", file=sys.stderr)
+
+ signal('scanned').send(self)
+
if quit:
sys.exit(1)
@@ -1089,6 +1267,13 @@ class Nikola(object):
deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS']
deps_dict['global'] = self.GLOBAL_CONTEXT
deps_dict['comments'] = context['enable_comments']
+
+ for k, v in self.GLOBAL_CONTEXT['template_hooks'].items():
+ deps_dict['||template_hooks|{0}||'.format(k)] = v._items
+
+ for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
+ deps_dict[k] = deps_dict['global'][k](lang)
+
if post:
deps_dict['post_translations'] = post.translated_to
@@ -1113,8 +1298,8 @@ class Nikola(object):
deps += post.deps(lang)
context = {}
context["posts"] = posts
- context["title"] = self.config['BLOG_TITLE']
- context["description"] = self.config['BLOG_DESCRIPTION']
+ context["title"] = self.config['BLOG_TITLE'](lang)
+ context["description"] = self.config['BLOG_DESCRIPTION'](lang)
context["lang"] = lang
context["prevlink"] = None
context["nextlink"] = None
@@ -1123,6 +1308,13 @@ class Nikola(object):
deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in
posts]
deps_context["global"] = self.GLOBAL_CONTEXT
+
+ for k, v in self.GLOBAL_CONTEXT['template_hooks'].items():
+ deps_context['||template_hooks|{0}||'.format(k)] = v._items
+
+ for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
+ deps_context[k] = deps_context['global'][k](lang)
+
task = {
'name': os.path.normpath(output_name),
'targets': [output_name],
@@ -1135,6 +1327,9 @@ class Nikola(object):
return utils.apply_filters(task, filters)
+ def __repr__(self):
+ return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE']())
+
def sanitized_locales(locale_fallback, locale_default, locales, translations):
"""Sanitizes all locales availble into a nikola session
@@ -1297,7 +1492,7 @@ _windows_locale_guesses = {
"en": "English",
"eo": "Esperanto",
"es": "Spanish",
- "fa": "Farsi", # persian
+ "fa": "Farsi", # Persian
"fr": "French",
"hr": "Croatian",
"it": "Italian",
@@ -1322,6 +1517,6 @@ SOCIAL_BUTTONS_CODE = """
<li><a class="addthis_button_twitter"></a>
</ul>
</div>
-<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
+<script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
<!-- End of social buttons -->
"""