diff options
Diffstat (limited to 'nikola/nikola.py')
| -rw-r--r-- | nikola/nikola.py | 1479 |
1 files changed, 134 insertions, 1345 deletions
diff --git a/nikola/nikola.py b/nikola/nikola.py index aa43398..8b69d02 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -1,34 +1,35 @@ # -*- coding: utf-8 -*- -import codecs from collections import defaultdict from copy import copy -import datetime import glob -import json import os -from StringIO import StringIO import sys -import tempfile -import urllib2 import urlparse -from doit.tools import PythonInteractiveAction import lxml.html -from pygments import highlight -from pygments.lexers import get_lexer_for_filename, TextLexer -from pygments.formatters import HtmlFormatter -try: - import webassets -except ImportError: - webassets = None +from yapsy.PluginManager import PluginManager + +if os.getenv('DEBUG'): + import logging + logging.basicConfig(level=logging.DEBUG) +else: + import logging + logging.basicConfig(level=logging.ERROR) from post import Post import utils +from plugin_categories import ( + Command, + LateTask, + PageCompiler, + Task, + TemplateSystem, +) config_changed = utils.config_changed -__all__ = ['Nikola', 'nikola_main'] +__all__ = ['Nikola'] class Nikola(object): @@ -68,6 +69,7 @@ class Nikola(object): 'FILTERS': {}, 'USE_BUNDLES': True, 'TAG_PAGES_ARE_INDEXES': False, + 'THEME': 'default', 'post_compilers': { "rest": ['.txt', '.rst'], "markdown": ['.md', '.mdown', '.markdown'], @@ -75,29 +77,48 @@ class Nikola(object): }, } self.config.update(config) - if not self.config['TRANSLATIONS']: - self.config['TRANSLATIONS']={ - self.config['DEFAULT_LANG']: ''} - - if self.config['USE_BUNDLES'] and not webassets: - self.config['USE_BUNDLES'] = False + self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS', + {self.config['DEFAULT_LANG']: ''}) - self.get_compile_html = utils.CompileHtmlGetter( - self.config.pop('post_compilers')) - - self.GLOBAL_CONTEXT = self.config['GLOBAL_CONTEXT'] self.THEMES = utils.get_theme_chain(self.config['THEME']) - self.templates_module = utils.get_template_module( - utils.get_template_engine(self.THEMES), self.THEMES) - self.template_deps = self.templates_module.template_deps - - self.theme_bundles = utils.get_theme_bundles(self.THEMES) - self.MESSAGES = utils.load_messages(self.THEMES, self.config['TRANSLATIONS']) - self.GLOBAL_CONTEXT['messages'] = self.MESSAGES + self.plugin_manager = PluginManager(categories_filter={ + "Command": Command, + "Task": Task, + "LateTask": LateTask, + "TemplateSystem": TemplateSystem, + "PageCompiler": PageCompiler, + }) + self.plugin_manager.setPluginInfoExtension('plugin') + self.plugin_manager.setPluginPlaces([ + os.path.join(os.path.dirname(__file__), 'plugins'), + os.path.join(os.getcwd(), 'plugins'), + ]) + self.plugin_manager.collectPlugins() + + self.commands = {} + # Activate all command plugins + for pluginInfo in self.plugin_manager.getPluginsOfCategory("Command"): + 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) + + # set global_context for template rendering + self.GLOBAL_CONTEXT = self.config.get('GLOBAL_CONTEXT', {}) + self.GLOBAL_CONTEXT['messages'] = self.MESSAGES self.GLOBAL_CONTEXT['_link'] = self.link self.GLOBAL_CONTEXT['rel_link'] = self.rel_link self.GLOBAL_CONTEXT['abs_link'] = self.abs_link @@ -108,19 +129,74 @@ class Nikola(object): 'INDEX_DISPLAY_POST_COUNT'] self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES'] - self.DEPS_CONTEXT = {} - for k, v in self.GLOBAL_CONTEXT.items(): - if isinstance(v, (str, unicode, int, float, dict)): - self.DEPS_CONTEXT[k] = v + # Load template plugin + template_sys_name = utils.get_template_engine(self.THEMES) + 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.exit(1) + self.template_system = pi.plugin_object + self.template_system.set_directories( + [os.path.join(utils.get_theme_path(name), "templates") + for name in self.THEMES]) + + # Load compiler plugins + self.compilers = {} + self.inverse_compilers = {} + + for pluginInfo in self.plugin_manager.getPluginsOfCategory( + "PageCompiler"): + self.compilers[pluginInfo.name] = \ + pluginInfo.plugin_object.compile_html + + def get_compiler(self, source_name): + """Get the correct compiler for a post from `conf.post_compilers` + + To make things easier for users, the mapping in conf.py is + compiler->[extensions], although this is less convenient for us. The + majority of this function is reversing that dictionary and error + checking. + """ + ext = os.path.splitext(source_name)[1] + try: + compile_html = self.inverse_compilers[ext] + except KeyError: + # Find the correct compiler for this files extension + langs = [lang for lang, exts in + self.config['post_compilers'].items() + if ext in exts] + if len(langs) != 1: + if len(set(langs)) > 1: + 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)) + elif len(langs) > 1: + langs = langs[:1] + else: + exit("post_compilers in conf.py does not tell me how to " + "handle '%s' extensions." % ext) + + lang = langs[0] + compile_html = self.compilers[lang] + self.inverse_compilers[ext] = compile_html + + return compile_html def render_template(self, template_name, output_name, context): - data = self.templates_module.render_template( - template_name, None, context, self.GLOBAL_CONTEXT) + local_context = {} + local_context["template_name"] = template_name + local_context.update(self.config['GLOBAL_CONTEXT']) + local_context.update(context) + data = self.template_system.render_template( + template_name, None, local_context) assert output_name.startswith(self.config["OUTPUT_FOLDER"]) url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:] - #this to support windows paths + # This is to support windows paths url_part = "/".join(url_part.split(os.sep)) src = urlparse.urljoin(self.config["BLOG_URL"], url_part) @@ -289,130 +365,39 @@ class Nikola(object): return exists def gen_tasks(self): + task_dep = [] + for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"): + for task in pluginInfo.plugin_object.gen_tasks(): + yield task + if pluginInfo.plugin_object.is_default: + task_dep.append(pluginInfo.plugin_object.name) - yield self.task_serve(output_folder=self.config['OUTPUT_FOLDER']) - yield self.task_install_theme() - yield self.task_bootswatch_theme() - yield self.gen_task_new_post(self.config['post_pages']) - yield self.gen_task_new_page(self.config['post_pages']) - yield self.gen_task_copy_assets(themes=self.THEMES, - output_folder=self.config['OUTPUT_FOLDER'], - filters=self.config['FILTERS'] - ) - if webassets: - yield self.gen_task_build_bundles(theme_bundles=self.theme_bundles, - output_folder=self.config['OUTPUT_FOLDER'], - filters=self.config['FILTERS'] - ) - yield self.gen_task_deploy(commands=self.config['DEPLOY_COMMANDS']) - yield self.gen_task_sitemap(blog_url=self.config['BLOG_URL'], - output_folder=self.config['OUTPUT_FOLDER'] - ) - yield self.gen_task_render_pages( - translations=self.config['TRANSLATIONS'], - post_pages=self.config['post_pages'], - filters=self.config['FILTERS']) - yield self.gen_task_render_sources( - translations=self.config['TRANSLATIONS'], - default_lang=self.config['DEFAULT_LANG'], - output_folder=self.config['OUTPUT_FOLDER'], - post_pages=self.config['post_pages']) - yield self.gen_task_render_posts( - translations=self.config['TRANSLATIONS'], - default_lang=self.config['DEFAULT_LANG'], - timeline=self.timeline - ) - yield self.gen_task_render_indexes( - translations=self.config['TRANSLATIONS'], - messages=self.MESSAGES, - output_folder=self.config['OUTPUT_FOLDER'], - index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'], - index_teasers=self.config['INDEX_TEASERS'], - filters=self.config['FILTERS'], - ) - yield self.gen_task_render_archive( - translations=self.config['TRANSLATIONS'], - messages=self.MESSAGES, - output_folder=self.config['OUTPUT_FOLDER'], - filters=self.config['FILTERS'], - ) - yield self.gen_task_render_tags( - translations=self.config['TRANSLATIONS'], - messages=self.MESSAGES, - blog_title=self.config['BLOG_TITLE'], - blog_url=self.config['BLOG_URL'], - blog_description=self.config['BLOG_DESCRIPTION'], - output_folder=self.config['OUTPUT_FOLDER'], - filters=self.config['FILTERS'], - tag_pages_are_indexes=self.config['TAG_PAGES_ARE_INDEXES'], - index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'], - index_teasers=self.config['INDEX_TEASERS'], - ) - yield self.gen_task_render_rss( - translations=self.config['TRANSLATIONS'], - blog_title=self.config['BLOG_TITLE'], - blog_url=self.config['BLOG_URL'], - blog_description=self.config['BLOG_DESCRIPTION'], - output_folder=self.config['OUTPUT_FOLDER']) - yield self.gen_task_render_galleries( - max_image_size=self.config['MAX_IMAGE_SIZE'], - thumbnail_size=self.config['THUMBNAIL_SIZE'], - default_lang=self.config['DEFAULT_LANG'], - output_folder=self.config['OUTPUT_FOLDER'], - use_filename_as_title=self.config['USE_FILENAME_AS_TITLE'], - blog_description=self.config['BLOG_DESCRIPTION'] - ) - yield self.gen_task_render_listings( - listings_folder=self.config['LISTINGS_FOLDER'], - default_lang=self.config['DEFAULT_LANG'], - output_folder=self.config['OUTPUT_FOLDER']) - yield self.gen_task_redirect( - redirections=self.config['REDIRECTIONS'], - output_folder=self.config['OUTPUT_FOLDER']) - yield self.gen_task_copy_files( - output_folder=self.config['OUTPUT_FOLDER'], - files_folders=self.config['FILES_FOLDERS'], - filters=self.config['FILTERS']) - - task_dep = [ - 'render_listings', - 'render_archive', - 'render_galleries', - 'render_indexes', - 'render_pages', - 'render_posts', - 'render_rss', - 'render_sources', - 'render_tags', - 'copy_assets', - 'copy_files', - 'sitemap', - 'redirect' - ] - - if webassets: - task_dep.append( 'build_bundles' ) + for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"): + for task in pluginInfo.plugin_object.gen_tasks(): + yield task + if pluginInfo.plugin_object.is_default: + task_dep.append(pluginInfo.plugin_object.name) yield { 'name': 'all', 'actions': None, 'clean': True, 'task_dep': task_dep - } + } def scan_posts(self): """Scan all the posts.""" if not self._scanned: print "Scanning posts ", targets = set([]) - for wildcard, destination, _, use_in_feeds in self.config['post_pages']: + for wildcard, destination, _, use_in_feeds in \ + self.config['post_pages']: print ".", for base_path in glob.glob(wildcard): post = Post(base_path, destination, use_in_feeds, self.config['TRANSLATIONS'], self.config['DEFAULT_LANG'], self.config['BLOG_URL'], - self.get_compile_html(base_path), self.MESSAGES) for lang, langpath in self.config['TRANSLATIONS'].items(): dest = (destination, langpath, post.pagenames[lang]) @@ -448,7 +433,8 @@ class Nikola(object): post_name = os.path.splitext(post)[0] context = {} post = self.global_data[post_name] - deps = post.deps(lang) + self.template_deps(template_name) + deps = post.deps(lang) + \ + self.template_system.template_deps(template_name) context['post'] = post context['lang'] = lang context['title'] = post.title(lang) @@ -468,6 +454,7 @@ class Nikola(object): deps_dict['NEXT_LINK'] = [post.next_post.permalink(lang)] deps_dict['OUTPUT_FOLDER'] = self.config['OUTPUT_FOLDER'] deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS'] + deps_dict['global'] = self.config['GLOBAL_CONTEXT'] task = { 'name': output_name.encode('utf-8'), @@ -481,187 +468,11 @@ class Nikola(object): yield utils.apply_filters(task, filters) - def gen_task_render_pages(self, **kw): - """Build final pages from metadata and HTML fragments. - - Required keyword arguments: - - translations - post_pages - """ - self.scan_posts() - flag = False - for lang in kw["translations"]: - for wildcard, destination, template_name, _ in kw["post_pages"]: - for task in self.generic_page_renderer(lang, - wildcard, template_name, destination, kw["filters"]): - # TODO: enable or remove - #task['uptodate'] = task.get('uptodate', []) +\ - #[config_changed(kw)] - task['basename'] = 'render_pages' - flag = True - yield task - if flag == False: # No page rendered, yield a dummy task - yield { - 'basename': 'render_pages', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - def gen_task_render_sources(self, **kw): - """Publish the rst sources because why not? - - Required keyword arguments: - - translations - default_lang - post_pages - output_folder - """ - self.scan_posts() - flag = False - for lang in kw["translations"]: - # TODO: timeline is global - for post in self.timeline: - output_name = os.path.join(kw['output_folder'], - post.destination_path(lang, post.source_ext())) - source = post.source_path - if lang != kw["default_lang"]: - source_lang = source + '.' + lang - if os.path.exists(source_lang): - source = source_lang - yield { - 'basename': 'render_sources', - 'name': output_name.encode('utf8'), - 'file_dep': [source], - 'targets': [output_name], - 'actions': [(utils.copy_file, (source, output_name))], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - if flag == False: # No page rendered, yield a dummy task - yield { - 'basename': 'render_sources', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - def gen_task_render_posts(self, **kw): - """Build HTML fragments from metadata and reSt. - - Required keyword arguments: - - translations - default_lang - timeline - """ - self.scan_posts() - flag = False - for lang in kw["translations"]: - # TODO: timeline is global, get rid of it - deps_dict = copy(kw) - deps_dict.pop('timeline') - for post in kw['timeline']: - source = post.source_path - dest = post.base_path - if lang != kw["default_lang"]: - dest += '.' + lang - source_lang = source + '.' + lang - if os.path.exists(source_lang): - source = source_lang - flag = True - yield { - 'basename': 'render_posts', - 'name': dest.encode('utf-8'), - 'file_dep': post.fragment_deps(lang), - 'targets': [dest], - 'actions': [(post.compile_html, [source, dest])], - 'clean': True, - 'uptodate': [config_changed(deps_dict)], - } - if flag == False: # Return a dummy task - yield { - 'basename': 'render_posts', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - def gen_task_render_indexes(self, **kw): - """Render post-per-page indexes. - The default is 10. - - Required keyword arguments: - - translations - output_folder - index_display_post_count - index_teasers - """ - self.scan_posts() - template_name = "index.tmpl" - # TODO: timeline is global, get rid of it - posts = [x for x in self.timeline if x.use_in_feeds] - # Split in smaller lists - lists = [] - while posts: - lists.append(posts[:kw["index_display_post_count"]]) - posts = posts[kw["index_display_post_count"]:] - num_pages = len(lists) - if not lists: - yield { - 'basename': 'render_indexes', - 'actions': [], - } - for lang in kw["translations"]: - for i, post_list in enumerate(lists): - context = {} - if self.config.get("INDEXES_TITLE", ""): - indexes_title = self.config['INDEXES_TITLE'] - else: - indexes_title = self.config["BLOG_TITLE"] - if not i: - output_name = "index.html" - context["title"] = indexes_title - else: - output_name = "index-%s.html" % i - if self.config.get("INDEXES_PAGES", ""): - indexes_pages = self.config["INDEXES_PAGES"] % i - else: - indexes_pages = " (" + kw["messages"][lang]["old posts page %d"] % i + ")" - context["title"] = indexes_title + indexes_pages - context["prevlink"] = None - context["nextlink"] = None - context['index_teasers'] = kw['index_teasers'] - if i > 1: - context["prevlink"] = "index-%s.html" % (i - 1) - if i == 1: - context["prevlink"] = "index.html" - if i < num_pages - 1: - context["nextlink"] = "index-%s.html" % (i + 1) - context["permalink"] = self.link("index", i, lang) - output_name = os.path.join( - kw['output_folder'], self.path("index", i, lang)) - for task in self.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - task['basename'] = 'render_indexes' - yield task - def generic_post_list_renderer(self, lang, posts, output_name, template_name, filters, extra_context): """Renders pages with lists of posts.""" - deps = self.template_deps(template_name) + deps = self.template_system.template_deps(template_name) for post in posts: deps += post.deps(lang) context = {} @@ -675,6 +486,7 @@ class Nikola(object): deps_context = copy(context) deps_context["posts"] = [(p.titles[lang], p.permalink(lang)) for p in posts] + deps_context["global"] = self.config['GLOBAL_CONTEXT'] task = { 'name': output_name.encode('utf8'), 'targets': [output_name], @@ -686,1026 +498,3 @@ class Nikola(object): } yield utils.apply_filters(task, filters) - - def gen_task_render_archive(self, **kw): - """Render the post archives. - - Required keyword arguments: - - translations - messages - output_folder - """ - # TODO add next/prev links for years - template_name = "list.tmpl" - # TODO: posts_per_year is global, kill it - for year, posts in self.posts_per_year.items(): - for lang in kw["translations"]: - output_name = os.path.join( - kw['output_folder'], self.path("archive", year, lang)) - post_list = [self.global_data[post] for post in posts] - post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) - post_list.reverse() - context = {} - context["lang"] = lang - context["items"] = [("[%s] %s" % - (post.date, post.title(lang)), post.permalink(lang)) - for post in post_list] - context["permalink"] = self.link("archive", year, lang) - context["title"] = kw["messages"][lang]["Posts for year %s"]\ - % year - for task in self.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - yield task - - # And global "all your years" page - years = self.posts_per_year.keys() - years.sort(reverse=True) - template_name = "list.tmpl" - kw['years'] = years - for lang in kw["translations"]: - context = {} - output_name = os.path.join( - kw['output_folder'], self.path("archive", None, lang)) - context["title"] = kw["messages"][lang]["Archive"] - context["items"] = [(year, self.link("archive", year, lang)) - for year in years] - context["permalink"] = self.link("archive", None, lang) - for task in self.generic_post_list_renderer( - lang, - [], - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - task['basename'] = 'render_archive' - yield task - - def gen_task_render_tags(self, **kw): - """Render the tag pages. - - Required keyword arguments: - - translations - messages - blog_title - blog_url - blog_description - output_folder - tag_pages_are_indexes - index_display_post_count - index_teasers - """ - if not self.posts_per_tag: - yield { - 'basename': 'render_tags', - 'actions': [], - } - return - def page_name(tagname, i, lang): - """Given tag, n, returns a page name.""" - name = self.path("tag", tag, lang) - if i: - name = name.replace('.html', '-%s.html' % i) - return name - - for tag, posts in self.posts_per_tag.items(): - post_list = [self.global_data[post] for post in posts] - post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) - post_list.reverse() - for lang in kw["translations"]: - #Render RSS - output_name = os.path.join(kw['output_folder'], - self.path("tag_rss", tag, lang)) - deps = [] - post_list = [self.global_data[post] for post in posts - if self.global_data[post].use_in_feeds] - post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) - post_list.reverse() - for post in post_list: - deps += post.deps(lang) - yield { - 'name': output_name.encode('utf8'), - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, "%s (%s)" % (kw["blog_title"], tag), - kw["blog_url"], kw["blog_description"], - post_list, output_name))], - 'clean': True, - 'uptodate': [config_changed(kw)], - 'basename': 'render_tags' - } - - # Render HTML - if kw['tag_pages_are_indexes']: - # We render a sort of index page collection using only - # this tag's posts. - - # FIXME: deduplicate this with render_indexes - template_name = "index.tmpl" - # Split in smaller lists - lists = [] - while post_list: - lists.append(post_list[:kw["index_display_post_count"]]) - post_list = post_list[kw["index_display_post_count"]:] - num_pages = len(lists) - for i, post_list in enumerate(lists): - context = {} - # On a tag page, the feeds are the tag's feeds, plus the site's - rss_link = \ - """<link rel="alternate" type="application/rss+xml" """\ - """type="application/rss+xml" title="RSS for tag """\ - """%s (%s)" href="%s">""" % \ - (tag, lang, self.link("tag_rss", tag, lang)) - context ['rss_link'] = rss_link - output_name = os.path.join(kw['output_folder'], - page_name(tag, i, lang)) - context["title"] = kw["messages"][lang][u"Posts about %s:"]\ - % tag - context["prevlink"] = None - context["nextlink"] = None - context['index_teasers'] = kw['index_teasers'] - if i > 1: - context["prevlink"] = os.path.basename(page_name(tag, i - 1, lang)) - if i == 1: - context["prevlink"] = os.path.basename(page_name(tag, 0, lang)) - if i < num_pages - 1: - context["nextlink"] = os.path.basename(page_name(tag, i + 1, lang)) - context["permalink"] = self.link("tag", tag, lang) - context["tag"] = tag - for task in self.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - task['basename'] = 'render_tags' - yield task - else: - # We render a single flat link list with this tag's posts - template_name = "tag.tmpl" - output_name = os.path.join(kw['output_folder'], - self.path("tag", tag, lang)) - context = {} - context["lang"] = lang - context["title"] = kw["messages"][lang][u"Posts about %s:"]\ - % tag - context["items"] = [("[%s] %s" % (post.date, post.title(lang)), - post.permalink(lang)) for post in post_list] - context["permalink"] = self.link("tag", tag, lang) - context["tag"] = tag - for task in self.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - task['basename'] = 'render_tags' - yield task - - # And global "all your tags" page - tags = self.posts_per_tag.keys() - tags.sort() - template_name = "tags.tmpl" - kw['tags'] = tags - for lang in kw["translations"]: - output_name = os.path.join( - kw['output_folder'], self.path('tag_index', None, lang)) - context = {} - context["title"] = kw["messages"][lang][u"Tags"] - context["items"] = [(tag, self.link("tag", tag, lang)) - for tag in tags] - context["permalink"] = self.link("tag_index", None, lang) - for task in self.generic_post_list_renderer( - lang, - [], - output_name, - template_name, - kw['filters'], - context, - ): - task['uptodate'] = task.get('updtodate', []) +\ - [config_changed(kw)] - yield task - - def gen_task_render_rss(self, **kw): - """Generate RSS feeds. - - Required keyword arguments: - - translations - blog_title - blog_url - blog_description - output_folder - """ - - self.scan_posts() - # TODO: timeline is global, kill it - for lang in kw["translations"]: - output_name = os.path.join(kw['output_folder'], - self.path("rss", None, lang)) - deps = [] - posts = [x for x in self.timeline if x.use_in_feeds][:10] - for post in posts: - deps += post.deps(lang) - yield { - 'basename': 'render_rss', - 'name': output_name, - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"], kw["blog_url"], - kw["blog_description"], posts, output_name))], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - def gen_task_render_listings(self, **kw): - """ - Required keyword arguments: - - listings_folder - output_folder - default_lang - """ - - # Things to ignore in listings - ignored_extensions = (".pyc",) - - def render_listing(in_name, out_name): - with open(in_name, 'r') as fd: - try: - lexer = get_lexer_for_filename(in_name) - except: - lexer = TextLexer() - code = highlight(fd.read(), lexer, - HtmlFormatter(cssclass='code', - linenos="table", - nowrap=False, - lineanchors=utils.slugify(f), - anchorlinenos=True)) - title = os.path.basename(in_name) - crumbs = out_name.split(os.sep)[1:-1] + [title] - # TODO: write this in human - paths = ['/'.join(['..'] * (len(crumbs) - 2 - i)) for i in range(len(crumbs[:-2]))] + ['.', '#'] - context = { - 'code': code, - 'title': title, - 'crumbs': zip(paths, crumbs), - 'lang': kw['default_lang'], - 'description': title, - } - self.render_template('listing.tmpl', out_name, context) - flag = True - template_deps = self.template_deps('listing.tmpl') - for root, dirs, files in os.walk(kw['listings_folder']): - # Render all files - for f in files: - ext = os.path.splitext(f)[-1] - if ext in ignored_extensions: - continue - flag = False - in_name = os.path.join(root, f) - out_name = os.path.join( - kw['output_folder'], - root, - f) + '.html' - yield { - 'basename': 'render_listings', - 'name': out_name.encode('utf8'), - 'file_dep': template_deps + [in_name], - 'targets': [out_name], - 'actions': [(render_listing, [in_name, out_name])], - } - if flag: - yield { - 'basename': 'render_listings', - 'actions': [], - } - - def gen_task_render_galleries(self, **kw): - """Render image galleries. - - Required keyword arguments: - - image_size - thumbnail_size, - default_lang, - output_folder, - use_filename_as_title - """ - - # FIXME: lots of work is done even when images don't change, - # which should be moved into the task. - # Also, this is getting complex enough to be refactored into a file. - - template_name = "gallery.tmpl" - - gallery_list = glob.glob("galleries/*") - # Fail quick if we don't have galleries, so we don't - # require PIL - Image = None - if not gallery_list: - yield { - 'basename': 'render_galleries', - 'actions': [], - } - return - try: - import Image as _Image - import ExifTags - Image = _Image - except ImportError: - try: - from PIL import Image as _Image, ExifTags - Image = _Image - except ImportError: - pass - if Image: - def _resize_image(src, dst, max_size): - im = Image.open(src) - w, h = im.size - if w > max_size or h > max_size: - size = max_size, max_size - try: - exif = im._getexif() - except Exception: - exif = None - if exif is not None: - for tag, value in exif.items(): - decoded = ExifTags.TAGS.get(tag, tag) - - if decoded == 'Orientation': - if value == 3: - im = im.rotate(180) - elif value == 6: - im = im.rotate(270) - elif value == 8: - im = im.rotate(90) - - break - - im.thumbnail(size, Image.ANTIALIAS) - im.save(dst) - - else: - utils.copy_file(src, dst) - - def create_thumb(src, dst): - return _resize_image(src, dst, kw['thumbnail_size']) - - def create_resized_image(src, dst): - return _resize_image(src, dst, kw['max_image_size']) - - dates = {} - def image_date(src): - if src not in dates: - im = Image.open(src) - try: - exif = im._getexif() - except Exception: - exif = None - if exif is not None: - for tag, value in exif.items(): - decoded = ExifTags.TAGS.get(tag, tag) - if decoded == 'DateTimeOriginal': - try: - dates[src] = datetime.datetime.strptime(value, r'%Y:%m:%d %H:%M:%S') - break - except ValueError: #invalid EXIF date - pass - if src not in dates: - dates[src] = datetime.datetime.fromtimestamp(os.stat(src).st_mtime) - return dates[src] - - else: - create_thumb = utils.copy_file - create_resized_image = utils.copy_file - - # gallery_path is "gallery/name" - for gallery_path in gallery_list: - # gallery_name is "name" - gallery_name = os.path.basename(gallery_path) - # output_gallery is "output/GALLERY_PATH/name" - output_gallery = os.path.dirname(os.path.join(kw["output_folder"], - self.path("gallery", gallery_name, None))) - if not os.path.isdir(output_gallery): - yield { - 'basename': 'render_galleries', - 'name': output_gallery, - 'actions': [(os.makedirs, (output_gallery,))], - 'targets': [output_gallery], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - # image_list contains "gallery/name/image_name.jpg" - image_list = glob.glob(gallery_path + "/*jpg") +\ - glob.glob(gallery_path + "/*JPG") +\ - glob.glob(gallery_path + "/*PNG") +\ - glob.glob(gallery_path + "/*png") - - # Filter ignore images - try: - def add_gallery_path(index): - return "{0}/{1}".format(gallery_path, index) - - exclude_path = os.path.join(gallery_path, "exclude.meta") - try: - f = open(exclude_path, 'r') - excluded_image_name_list = f.read().split() - except IOError: - excluded_image_name_list = [] - - excluded_image_list = map(add_gallery_path, - excluded_image_name_list) - image_set = set(image_list) - set(excluded_image_list) - image_list = list(image_set) - except IOError: - pass - - image_list = [x for x in image_list if "thumbnail" not in x] - # Sort by date - image_list.sort(cmp=lambda a,b: cmp(image_date(a), image_date(b))) - image_name_list = [os.path.basename(x) for x in image_list] - - thumbs = [] - # Do thumbnails and copy originals - for img, img_name in zip(image_list, image_name_list): - # img is "galleries/name/image_name.jpg" - # img_name is "image_name.jpg" - # fname, ext are "image_name", ".jpg" - fname, ext = os.path.splitext(img_name) - # thumb_path is - # "output/GALLERY_PATH/name/image_name.thumbnail.jpg" - thumb_path = os.path.join(output_gallery, - fname + ".thumbnail" + ext) - # thumb_path is "output/GALLERY_PATH/name/image_name.jpg" - orig_dest_path = os.path.join(output_gallery, img_name) - thumbs.append(os.path.basename(thumb_path)) - yield { - 'basename': 'render_galleries', - 'name': thumb_path, - 'file_dep': [img], - 'targets': [thumb_path], - 'actions': [ - (create_thumb, (img, thumb_path)) - ], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - yield { - 'basename': 'render_galleries', - 'name': orig_dest_path, - 'file_dep': [img], - 'targets': [orig_dest_path], - 'actions': [ - (create_resized_image, (img, orig_dest_path)) - ], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - # Remove excluded images - if excluded_image_name_list: - for img, img_name in zip(excluded_image_list, - excluded_image_name_list): - # img_name is "image_name.jpg" - # fname, ext are "image_name", ".jpg" - fname, ext = os.path.splitext(img_name) - excluded_thumb_dest_path = os.path.join(output_gallery, - fname + ".thumbnail" + ext) - excluded_dest_path = os.path.join(output_gallery, img_name) - yield { - 'basename': 'render_galleries', - 'name': excluded_thumb_dest_path, - 'file_dep': [exclude_path], - #'targets': [excluded_thumb_dest_path], - 'actions': [ - (utils.remove_file, (excluded_thumb_dest_path,)) - ], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - yield { - 'basename': 'render_galleries', - 'name': excluded_dest_path, - 'file_dep': [exclude_path], - #'targets': [excluded_dest_path], - 'actions': [ - (utils.remove_file, (excluded_dest_path,)) - ], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - output_name = os.path.join(output_gallery, "index.html") - context = {} - context["lang"] = kw["default_lang"] - context["title"] = os.path.basename(gallery_path) - context["description"] = kw["blog_description"] - if kw['use_filename_as_title']: - img_titles = ['title="%s"' % utils.unslugify(fn[:-4]) - for fn in image_name_list] - else: - img_titles = [''] * len(image_name_list) - context["images"] = zip(image_name_list, thumbs, img_titles) - context["permalink"] = self.link("gallery", gallery_name, None) - - # Use galleries/name/index.txt to generate a blurb for - # the gallery, if it exists - index_path = os.path.join(gallery_path, "index.txt") - index_dst_path = os.path.join(gallery_path, "index.html") - if os.path.exists(index_path): - compile_html = self.get_compile_html(index_path) - yield { - 'basename': 'render_galleries', - 'name': index_dst_path.encode('utf-8'), - 'file_dep': [index_path], - 'targets': [index_dst_path], - 'actions': [(compile_html, - [index_path, index_dst_path])], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - file_dep = self.template_deps(template_name) + image_list - - def render_gallery(output_name, context, index_dst_path): - if os.path.exists(index_dst_path): - with codecs.open(index_dst_path, "rb", "utf8") as fd: - context['text'] = fd.read() - file_dep.append(index_dst_path) - else: - context['text'] = '' - self.render_template(template_name, output_name, context) - - yield { - 'basename': 'render_galleries', - 'name': gallery_path, - 'file_dep': file_dep, - 'targets': [output_name], - 'actions': [(render_gallery, - (output_name, context, index_dst_path))], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - @staticmethod - def gen_task_redirect(**kw): - """Generate redirections. - - Required keyword arguments: - - redirections - output_folder - """ - - def create_redirect(src, dst): - with codecs.open(src, "wb+", "utf8") as fd: - fd.write(('<head>' + - '<meta HTTP-EQUIV="REFRESH" content="0; url=%s">' + - '</head>') % dst) - - if not kw['redirections']: - # If there are no redirections, still needs to create a - # dummy action so dependencies don't fail - yield { - 'basename': 'redirect', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - else: - for src, dst in kw["redirections"]: - src_path = os.path.join(kw["output_folder"], src) - yield { - 'basename': 'redirect', - 'name': src_path, - 'targets': [src_path], - 'actions': [(create_redirect, (src_path, dst))], - 'clean': True, - 'uptodate': [config_changed(kw)], - } - - @staticmethod - def gen_task_copy_files(**kw): - """Copy static files into the output folder. - - required keyword arguments: - - output_folder - files_folders - """ - - flag = False - for src in kw['files_folders']: - dst = kw['output_folder'] - filters = kw['filters'] - real_dst = os.path.join(dst, kw['files_folders'][src]) - for task in utils.copy_tree(src, real_dst, link_cutoff=dst): - flag = True - task['basename'] = 'copy_files' - task['uptodate'] = task.get('uptodate', []) +\ - [config_changed(kw)] - yield utils.apply_filters(task, filters) - if not flag: - yield { - 'basename': 'copy_files', - 'actions': (), - } - - @staticmethod - def gen_task_copy_assets(**kw): - """Create tasks to copy the assets of the whole theme chain. - - If a file is present on two themes, use the version - from the "youngest" theme. - - Required keyword arguments: - - themes - output_folder - - """ - tasks = {} - for theme_name in kw['themes']: - 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: - continue - tasks[task['name']] = task - task['uptodate'] = task.get('uptodate', []) + \ - [config_changed(kw)] - task['basename'] = 'copy_assets' - yield utils.apply_filters(task, kw['filters']) - - @staticmethod - def gen_task_build_bundles(**kw): - """Create tasks to build bundles from theme assets. - - theme_bundles - output_folder - filters - """ - - def build_bundle(output, inputs): - env = webassets.Environment( - os.path.join(kw['output_folder'], os.path.dirname(output)), - os.path.dirname(output)) - bundle = webassets.Bundle(*inputs, - output=os.path.basename(output)) - env.register(output, bundle) - # This generates the file - env[output].urls() - - flag = False - for name, files in kw['theme_bundles'].items(): - output_path = os.path.join(kw['output_folder'], name) - dname = os.path.dirname(name) - file_dep = [os.path.join('output', dname, fname) - for fname in files] - task = { - 'task_dep': ['copy_assets', 'copy_files'], - 'file_dep': file_dep, - 'name': name, - 'actions': [(build_bundle, (name, files))], - 'targets': [os.path.join(kw['output_folder'], name)], - 'basename': 'build_bundles', - 'uptodate': [config_changed(kw)] - } - flag = True - yield utils.apply_filters(task, kw['filters']) - if flag == False: # No page rendered, yield a dummy task - yield { - 'basename': 'build_bundles', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - - @staticmethod - def new_post(post_pages, is_post=True): - # Guess where we should put this - for path, _, _, use_in_rss in post_pages: - if use_in_rss == is_post: - break - else: - path = post_pages[0][0] - - print "Creating New Post" - print "-----------------\n" - title = raw_input("Enter title: ").decode(sys.stdin.encoding) - slug = utils.slugify(title) - data = u'\n'.join([ - title, - slug, - datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') - ]) - output_path = os.path.dirname(path) - meta_path = os.path.join(output_path, slug + ".meta") - pattern = os.path.basename(path) - if pattern.startswith("*."): - suffix = pattern[1:] - else: - suffix = ".txt" - txt_path = os.path.join(output_path, slug + suffix) - - if os.path.isfile(meta_path) or os.path.isfile(txt_path): - print "The title already exists!" - exit() - - with codecs.open(meta_path, "wb+", "utf8") as fd: - fd.write(data) - with codecs.open(txt_path, "wb+", "utf8") as fd: - fd.write(u"Write your post here.") - print "Your post's metadata is at: ", meta_path - print "Your post's text is at: ", txt_path - - @classmethod - def new_page(cls): - cls.new_post(False) - - @classmethod - def gen_task_new_post(cls, post_pages): - """Create a new post (interactive).""" - yield { - "basename": "new_post", - "actions": [PythonInteractiveAction(cls.new_post, (post_pages,))], - } - - @classmethod - def gen_task_new_page(cls, post_pages): - """Create a new post (interactive).""" - yield { - "basename": "new_page", - "actions": [PythonInteractiveAction(cls.new_post, - (post_pages, False,))], - } - - @staticmethod - def gen_task_deploy(**kw): - """Deploy site. - - Required keyword arguments: - - commands - - """ - yield { - "basename": "deploy", - "actions": kw['commands'], - "verbosity": 2, - } - - @staticmethod - def gen_task_sitemap(**kw): - """Generate Google sitemap. - - Required keyword arguments: - - blog_url - output_folder - """ - - output_path = os.path.abspath(kw['output_folder']) - sitemap_path = os.path.join(output_path, "sitemap.xml.gz") - - def sitemap(): - # Generate config - config_data = """<?xml version="1.0" encoding="UTF-8"?> - <site - base_url="%s" - store_into="%s" - verbose="1" > - <directory path="%s" url="%s" /> - <filter action="drop" type="wildcard" pattern="*~" /> - <filter action="drop" type="regexp" pattern="/\.[^/]*" /> - </site>""" % ( - kw["blog_url"], - sitemap_path, - output_path, - kw["blog_url"], - ) - config_file = tempfile.NamedTemporaryFile(delete=False) - config_file.write(config_data) - config_file.close() - - # Generate sitemap - import sitemap_gen as smap - sitemap = smap.CreateSitemapFromFile(config_file.name, True) - if not sitemap: - smap.output.Log('Configuration file errors -- exiting.', 0) - else: - sitemap.Generate() - smap.output.Log('Number of errors: %d' % - smap.output.num_errors, 1) - smap.output.Log('Number of warnings: %d' % - smap.output.num_warns, 1) - os.unlink(config_file.name) - - yield { - "basename": "sitemap", - "task_dep": [ - "render_archive", - "render_indexes", - "render_pages", - "render_posts", - "render_rss", - "render_sources", - "render_tags"], - "targets": [sitemap_path], - "actions": [(sitemap,)], - "uptodate": [config_changed(kw)], - "clean": True, - } - - @staticmethod - def task_serve(**kw): - """ - Start test server. (doit serve [--address 127.0.0.1] [--port 8000]) - By default, the server runs on port 8000 on the IP address 127.0.0.1. - - required keyword arguments: - - output_folder - """ - - def serve(address, port): - from BaseHTTPServer import HTTPServer - from SimpleHTTPServer import SimpleHTTPRequestHandler - - class OurHTTPRequestHandler(SimpleHTTPRequestHandler): - extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) - extensions_map[""] = "text/plain" - - os.chdir(kw['output_folder']) - - httpd = HTTPServer((address, port), OurHTTPRequestHandler) - sa = httpd.socket.getsockname() - print "Serving HTTP on", sa[0], "port", sa[1], "..." - httpd.serve_forever() - - yield { - "basename": 'serve', - "actions": [(serve,)], - "verbosity": 2, - "params": [{'short': 'a', - 'name': 'address', - 'long': 'address', - 'type': str, - 'default': '127.0.0.1', - 'help': 'Bind address (default: 127.0.0.1)'}, - {'short': 'p', - 'name': 'port', - 'long': 'port', - 'type': int, - 'default': 8000, - 'help': 'Port number (default: 8000)'}], - } - - @staticmethod - def task_install_theme(): - """Install theme. (doit install_theme -n themename [-u URL]|[-l]).""" - - def install_theme(name, url, listing): - if name is None and not listing: - print "This command needs either the -n or the -l option." - return False - data = urllib2.urlopen(url).read() - data = json.loads(data) - if listing: - print "Themes:" - print "-------" - for theme in sorted(data.keys()): - print theme - return True - else: - if name in data: - if os.path.isfile("themes"): - raise IOError("'themes' isn't a directory!") - elif not os.path.isdir("themes"): - try: - os.makedirs("themes") - except: - raise OSError("mkdir 'theme' error!") - print 'Downloading: %s' % data[name] - zip_file = StringIO() - zip_file.write(urllib2.urlopen(data[name]).read()) - print 'Extracting: %s into themes' % name - utils.extract_all(zip_file) - else: - print "Can't find theme %s" % name - return False - - yield { - "basename": 'install_theme', - "actions": [(install_theme,)], - "verbosity": 2, - "params": [ - { - 'short': 'u', - 'name': 'url', - 'long': 'url', - 'type': str, - 'default': 'http://nikola.ralsina.com.ar/themes/index.json', - 'help': 'URL for theme collection.' - }, - { - 'short': 'l', - 'name': 'listing', - 'long': 'list', - 'type': bool, - 'default': False, - 'help': 'List available themes.' - }, - { - 'short': 'n', - 'name': 'name', - 'long': 'name', - 'type': str, - 'default': None, - 'help': 'Name of theme to install.' - }], - } - - @staticmethod - def task_bootswatch_theme(): - """Given a swatch name and a parent theme, creates a custom theme.""" - def bootswatch_theme(name, parent, swatch): - print "Creating %s theme from %s and %s" % (name, swatch, parent) - try: - os.makedirs(os.path.join('themes', name, 'assets', 'css')) - except: - pass - for fname in ('bootstrap.min.css', 'bootstrap.css'): - url = 'http://bootswatch.com/%s/%s' % (swatch, fname) - print "Downloading: ", url - data = urllib2.urlopen(url).read() - with open(os.path.join( - 'themes', name, 'assets', 'css', fname), 'wb+') as output: - output.write(data) - - with open(os.path.join('themes', name, 'parent'), 'wb+') as output: - output.write(parent) - print 'Theme created. Change the THEME setting to "%s" to use it.'\ - % name - - yield { - "basename": 'bootswatch_theme', - "actions": [(bootswatch_theme,)], - "verbosity": 2, - "params": [ - { - 'short': 'p', - 'name': 'parent', - 'long': 'parent', - 'type': str, - 'default': 'site', - 'help': 'Name of parent theme.' - }, - { - 'short': 's', - 'name': 'swatch', - 'long': 'swatch', - 'type': str, - 'default': 'slate', - 'help': 'Name of the swatch from bootswatch.com' - }, - { - 'short': 'n', - 'name': 'name', - 'long': 'name', - 'type': str, - 'default': 'custom', - 'help': 'Name of the new theme' - } - ], - } - - -def nikola_main(): - print "Starting doit..." - os.system("doit -f %s" % __file__) |
