diff options
| author | 2013-03-13 20:58:39 -0300 | |
|---|---|---|
| committer | 2013-03-13 20:58:39 -0300 | |
| commit | 8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (patch) | |
| tree | 0895935489e4920d18824f7fb3a0d799649a27c3 /nikola/nikola.py | |
| parent | 878ba1152ebc64a4a2609d23c9e400a6111db642 (diff) | |
Imported Upstream version 5.4.2upstream/5.4.2
Diffstat (limited to 'nikola/nikola.py')
| -rw-r--r-- | nikola/nikola.py | 249 |
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.""" |
