aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/nikola.py
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
commit8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (patch)
tree0895935489e4920d18824f7fb3a0d799649a27c3 /nikola/nikola.py
parent878ba1152ebc64a4a2609d23c9e400a6111db642 (diff)
Imported Upstream version 5.4.2upstream/5.4.2
Diffstat (limited to 'nikola/nikola.py')
-rw-r--r--nikola/nikola.py249
1 files changed, 171 insertions, 78 deletions
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 88de88c..a1506e7 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -27,6 +27,7 @@ from __future__ import print_function, unicode_literals
from collections import defaultdict
from copy import copy
import glob
+import gzip
import os
import sys
try:
@@ -36,6 +37,7 @@ except ImportError:
import lxml.html
from yapsy.PluginManager import PluginManager
+import pytz
if os.getenv('DEBUG'):
import logging
@@ -75,6 +77,10 @@ class Nikola(object):
self.timeline = []
self.pages = []
self._scanned = False
+ if not config:
+ self.configured = False
+ else:
+ self.configured = True
# This is the default config
# TODO: fill it
@@ -90,12 +96,15 @@ class Nikola(object):
'DATE_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': [],
+ 'DISABLED_PLUGINS': (),
'DISQUS_FORUM': 'nikolademo',
'FAVICONS': {},
'FILE_METADATA_REGEXP': None,
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
'GALLERY_PATH': 'galleries',
+ 'GZIP_FILES': False,
+ 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json'),
'INDEX_DISPLAY_POST_COUNT': 10,
'INDEX_TEASERS': False,
'INDEXES_TITLE': "",
@@ -104,6 +113,7 @@ class Nikola(object):
'LICENSE': '',
'LISTINGS_FOLDER': 'listings',
'MAX_IMAGE_SIZE': 1280,
+ 'MATHJAX_CONFIG': '',
'OUTPUT_FOLDER': 'output',
'post_compilers': {
"rest": ('.txt', '.rst'),
@@ -129,10 +139,13 @@ class Nikola(object):
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
'THEME': 'site',
+ 'THEME_REVEAL_CONGIF_SUBTHEME': 'sky',
+ 'THEME_REVEAL_CONGIF_TRANSITION': 'cube',
'THUMBNAIL_SIZE': 180,
'USE_BUNDLES': True,
'USE_CDN': False,
'USE_FILENAME_AS_TITLE': True,
+ 'TIMEZONE': None,
}
self.config.update(config)
@@ -145,6 +158,18 @@ class Nikola(object):
self.MESSAGES = utils.load_messages(self.THEMES,
self.config['TRANSLATIONS'])
+ # SITE_URL is required, but if the deprecated BLOG_URL
+ # is available, use it and warn
+ if 'SITE_URL' not in self.config:
+ if 'BLOG_URL' in self.config:
+ print("WARNING: You should configure SITE_URL instead of BLOG_URL")
+ print("See docs at FIXME put URL")
+ self.config['SITE_URL'] = self.config['BLOG_URL']
+
+ # BASE_URL defaults to SITE_URL
+ if 'BASE_URL' not in self.config:
+ self.config['BASE_URL'] = self.config.get('SITE_URL')
+
self.plugin_manager = PluginManager(categories_filter={
"Command": Command,
"Task": Task,
@@ -157,24 +182,28 @@ class Nikola(object):
str(os.path.join(os.path.dirname(__file__), 'plugins')),
str(os.path.join(os.getcwd(), 'plugins')),
])
+
self.plugin_manager.collectPlugins()
self.commands = {}
# Activate all command plugins
for pluginInfo in self.plugin_manager.getPluginsOfCategory("Command"):
+ if pluginInfo.name in self.config['DISABLED_PLUGINS']:
+ self.plugin_manager.removePluginFromCategory(pluginInfo, "Command")
+ continue
self.plugin_manager.activatePluginByName(pluginInfo.name)
pluginInfo.plugin_object.set_site(self)
pluginInfo.plugin_object.short_help = pluginInfo.description
self.commands[pluginInfo.name] = pluginInfo.plugin_object
# Activate all task plugins
- for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
- self.plugin_manager.activatePluginByName(pluginInfo.name)
- pluginInfo.plugin_object.set_site(self)
-
- for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
- self.plugin_manager.activatePluginByName(pluginInfo.name)
- pluginInfo.plugin_object.set_site(self)
+ for task_type in ["Task", "LateTask"]:
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory(task_type):
+ if pluginInfo.name in self.config['DISABLED_PLUGINS']:
+ self.plugin_manager.removePluginFromCategory(pluginInfo, task_type)
+ continue
+ self.plugin_manager.activatePluginByName(pluginInfo.name)
+ pluginInfo.plugin_object.set_site(self)
# set global_context for template rendering
self.GLOBAL_CONTEXT = {
@@ -195,20 +224,29 @@ class Nikola(object):
self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
self.GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
self.GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
- self.GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT', '%Y-%m-%d %H:%M')
+ self.GLOBAL_CONTEXT['date_format'] = self.config.get(
+ '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['blog_url'] = self.config.get('BLOG_URL')
+
+ self.GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL'))
self.GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
self.GLOBAL_CONTEXT['analytics'] = self.config.get('ANALYTICS')
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['disqus_forum'] = self.config.get('DISQUS_FORUM')
- self.GLOBAL_CONTEXT['content_footer'] = self.config.get('CONTENT_FOOTER')
+ self.GLOBAL_CONTEXT['mathjax_config'] = self.config.get(
+ 'MATHJAX_CONFIG')
+ self.GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONGIF_SUBTHEME')
+ self.GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONGIF_TRANSITION')
+ self.GLOBAL_CONTEXT['content_footer'] = self.config.get(
+ 'CONTENT_FOOTER')
self.GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
self.GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
self.GLOBAL_CONTEXT['sidebar_links'] = self.config.get('SIDEBAR_LINKS')
+ self.GLOBAL_CONTEXT['twitter_card'] = self.config.get(
+ 'TWITTER_CARD', {})
self.GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
@@ -226,8 +264,8 @@ class Nikola(object):
pi = self.plugin_manager.getPluginByName(
template_sys_name, "TemplateSystem")
if pi is None:
- sys.stderr.write("Error loading %s template system plugin\n" %
- template_sys_name)
+ sys.stderr.write("Error loading {0} template system "
+ "plugin\n".format(template_sys_name))
sys.exit(1)
self.template_system = pi.plugin_object
lookup_dirs = [os.path.join(utils.get_theme_path(name), "templates")
@@ -265,12 +303,12 @@ class Nikola(object):
exit("Your file extension->compiler definition is"
"ambiguous.\nPlease remove one of the file extensions"
"from 'post_compilers' in conf.py\n(The error is in"
- "one of %s)" % ', '.join(langs))
+ "one of {0})".format(', '.join(langs)))
elif len(langs) > 1:
langs = langs[:1]
else:
exit("post_compilers in conf.py does not tell me how to "
- "handle '%s' extensions." % ext)
+ "handle '{0}' extensions.".format(ext))
lang = langs[0]
compile_html = self.compilers[lang]
@@ -292,10 +330,12 @@ class Nikola(object):
url_part = output_name.decode('utf8')[len(self.config["OUTPUT_FOLDER"])
+ 1:]
- # This is to support windows paths
- url_part = "/".join(url_part.split(os.sep))
-
- src = urljoin(self.config["BLOG_URL"], url_part)
+ # Treat our site as if output/ is "/" and then make all URLs relative,
+ # making the site "relocatable"
+ src = os.sep + url_part
+ src = os.path.normpath(src)
+ # The os.sep is because normpath will change "/" to "\" on windows
+ src = "/".join(src.split(os.sep))
parsed_src = urlsplit(src)
src_elems = parsed_src.path.split('/')[1:]
@@ -398,7 +438,7 @@ class Nikola(object):
if name not in [None, 0]:
path = [_f for _f in [self.config['TRANSLATIONS'][lang],
self.config['INDEX_PATH'],
- 'index-%s.html' % name] if _f]
+ 'index-{0}.html'.format(name)] if _f]
else:
path = [_f for _f in [self.config['TRANSLATIONS'][lang],
self.config['INDEX_PATH'], 'index.html']
@@ -434,13 +474,13 @@ class Nikola(object):
def abs_link(self, dst):
# Normalize
- dst = urljoin(self.config['BLOG_URL'], dst)
+ dst = urljoin(self.config['BASE_URL'], dst)
return urlparse(dst).path
def rel_link(self, src, dst):
# Normalize
- src = urljoin(self.config['BLOG_URL'], src)
+ src = urljoin(self.config['BASE_URL'], src)
dst = urljoin(src, dst)
# Avoid empty links.
if src == dst:
@@ -471,19 +511,67 @@ class Nikola(object):
return exists
def gen_tasks(self):
- task_dep = []
+
+ def create_gzipped_copy(in_path, out_path):
+ with gzip.GzipFile(out_path, 'wb+') as outf:
+ with open(in_path, 'rb') as inf:
+ outf.write(inf.read())
+
+ def flatten(task):
+ if isinstance(task, dict):
+ yield task
+ else:
+ for t in task:
+ for ft in flatten(t):
+ yield ft
+
+ def add_gzipped_copies(task):
+ if not self.config['GZIP_FILES']:
+ return None
+ gzip_task = {
+ 'file_dep': [],
+ 'targets': [],
+ 'actions': [],
+ 'basename': 'gzip',
+ 'name': task.get('name', 'unknown'),
+ 'clean': True,
+ }
+ targets = task.get('targets', [])
+ flag = False
+ for target in targets:
+ ext = os.path.splitext(target)[1]
+ if (ext.lower() in self.config['GZIP_EXTENSIONS'] and
+ target.startswith(self.config['OUTPUT_FOLDER'])):
+ flag = True
+ gzipped = target + '.gz'
+ gzip_task['file_dep'].append(target)
+ gzip_task['targets'].append(gzipped)
+ gzip_task['actions'].append((create_gzipped_copy, (target, gzipped)))
+ if not flag:
+ return None
+ return gzip_task
+
+ if self.config['GZIP_FILES']:
+ task_dep = ['gzip']
+ else:
+ task_dep = []
for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
- for task in pluginInfo.plugin_object.gen_tasks():
+ for task in flatten(pluginInfo.plugin_object.gen_tasks()):
+ gztask = add_gzipped_copies(task)
+ if gztask:
+ yield gztask
yield task
if pluginInfo.plugin_object.is_default:
task_dep.append(pluginInfo.plugin_object.name)
for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
for task in pluginInfo.plugin_object.gen_tasks():
+ gztask = add_gzipped_copies(task)
+ if gztask:
+ yield gztask
yield task
if pluginInfo.plugin_object.is_default:
task_dep.append(pluginInfo.plugin_object.name)
-
yield {
'name': b'all',
'actions': None,
@@ -493,60 +581,65 @@ class Nikola(object):
def scan_posts(self):
"""Scan all the posts."""
- if not self._scanned:
- print("Scanning posts", end='')
- targets = set([])
- for wildcard, destination, template_name, use_in_feeds in \
- self.config['post_pages']:
- print(".", end='')
- base_len = len(destination.split(os.sep))
- dirname = os.path.dirname(wildcard)
- for dirpath, _, _ in os.walk(dirname):
- dir_glob = os.path.join(dirpath,
- os.path.basename(wildcard))
- dest_dir = os.path.join(*([destination] +
- dirpath.split(
- os.sep)[base_len:]))
- for base_path in glob.glob(dir_glob):
- post = Post(
- base_path,
- self.config['CACHE_FOLDER'],
- dest_dir,
- use_in_feeds,
- self.config['TRANSLATIONS'],
- self.config['DEFAULT_LANG'],
- self.config['BLOG_URL'],
- self.MESSAGES,
- template_name,
- self.config['FILE_METADATA_REGEXP'])
- for lang, langpath in list(
- self.config['TRANSLATIONS'].items()):
- dest = (destination, langpath, dir_glob,
- post.pagenames[lang])
- if dest in targets:
- raise Exception(
- 'Duplicated output path %r in post %r' %
- (post.pagenames[lang], base_path))
- targets.add(dest)
- self.global_data[post.post_name] = post
- if post.use_in_feeds:
- self.posts_per_year[
- str(post.date.year)].append(post.post_name)
- for tag in post.tags:
- self.posts_per_tag[tag].append(post.post_name)
- else:
- self.pages.append(post)
- for name, post in list(self.global_data.items()):
- self.timeline.append(post)
- 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._scanned = True
- print("done!")
+ if self._scanned:
+ return
+
+ print("Scanning posts", end='')
+ tzinfo = None
+ if self.config['TIMEZONE'] is not None:
+ tzinfo = pytz.timezone(self.config['TIMEZONE'])
+ targets = set([])
+ for wildcard, destination, template_name, use_in_feeds in \
+ self.config['post_pages']:
+ print(".", end='')
+ dirname = os.path.dirname(wildcard)
+ for dirpath, _, _ in os.walk(dirname):
+ dir_glob = os.path.join(dirpath, os.path.basename(wildcard))
+ dest_dir = os.path.normpath(os.path.join(destination,
+ os.path.relpath(dirpath, dirname)))
+ for base_path in glob.glob(dir_glob):
+ post = Post(
+ base_path,
+ self.config['CACHE_FOLDER'],
+ dest_dir,
+ use_in_feeds,
+ self.config['TRANSLATIONS'],
+ self.config['DEFAULT_LANG'],
+ self.config['BASE_URL'],
+ self.MESSAGES,
+ template_name,
+ self.config['FILE_METADATA_REGEXP'],
+ tzinfo,
+ )
+ for lang, langpath in list(
+ self.config['TRANSLATIONS'].items()):
+ dest = (destination, langpath, dir_glob,
+ post.pagenames[lang])
+ if dest in targets:
+ raise Exception('Duplicated output path {0!r} '
+ 'in post {1!r}'.format(
+ post.pagenames[lang],
+ base_path))
+ targets.add(dest)
+ self.global_data[post.post_name] = post
+ if post.use_in_feeds:
+ self.posts_per_year[
+ str(post.date.year)].append(post.post_name)
+ for tag in post.tags:
+ self.posts_per_tag[tag].append(post.post_name)
+ else:
+ self.pages.append(post)
+ for name, post in list(self.global_data.items()):
+ self.timeline.append(post)
+ 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._scanned = True
+ print("done!")
def generic_page_renderer(self, lang, post, filters):
"""Render post fragments to final HTML pages."""