diff options
| author | 2013-11-20 16:58:50 -0300 | |
|---|---|---|
| committer | 2013-11-20 16:58:50 -0300 | |
| commit | ca94afc07df55cb7fc6fe3b4f3011877b7881195 (patch) | |
| tree | d81e1f275aa77545f33740723f307a83dde2e0b4 /nikola/plugins/task | |
| parent | f794eee787e9cde54e6b8f53e45d69c9ddc9936a (diff) | |
Imported Upstream version 6.2.1upstream/6.2.1
Diffstat (limited to 'nikola/plugins/task')
48 files changed, 3590 insertions, 0 deletions
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/plugins/task/__init__.py diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin new file mode 100644 index 0000000..448b115 --- /dev/null +++ b/nikola/plugins/task/archive.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_archive +Module = archive + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Generates the blog's archive pages. + diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py new file mode 100644 index 0000000..3afbea1 --- /dev/null +++ b/nikola/plugins/task/archive.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import os + +# for tearDown with _reload we cannot use 'import from' to access LocaleBorg +import nikola.utils +from nikola.plugin_categories import Task +from nikola.utils import config_changed + + +class Archive(Task): + """Render the post archives.""" + + name = "render_archive" + + def set_site(self, site): + site.register_path_handler('archive', self.archive_path) + return super(Archive, self).set_site(site) + + def gen_tasks(self): + kw = { + "messages": self.site.MESSAGES, + "translations": self.site.config['TRANSLATIONS'], + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'], + "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'], + } + self.site.scan_posts() + yield self.group_task() + # TODO add next/prev links for years + if kw['create_monthly_archive'] and kw['create_single_archive']: + raise Exception('Cannot create monthly and single archives at the same time.') + for lang in kw["translations"]: + archdata = self.site.posts_per_year + # A bit of a hack. + if kw['create_single_archive']: + archdata = {None: self.site.posts} + + for year, posts in archdata.items(): + output_name = os.path.join( + kw['output_folder'], self.site.path("archive", year, lang)) + context = {} + context["lang"] = lang + if year: + context["title"] = kw["messages"][lang]["Posts for year %s"] % year + else: + context["title"] = kw["messages"][lang]["Archive"] + context["permalink"] = self.site.link("archive", year, lang) + if not kw["create_monthly_archive"]: + template_name = "list_post.tmpl" + post_list = [self.site.global_data[post] for post in posts] + post_list.sort(key=lambda a: a.date) + post_list.reverse() + context["posts"] = post_list + else: # Monthly archives, just list the months + months = set([m.split('/')[1] for m in self.site.posts_per_month.keys() if m.startswith(str(year))]) + months = sorted(list(months)) + months.reverse() + template_name = "list.tmpl" + context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), month] for month in months] + post_list = [] + task = self.site.generic_post_list_renderer( + lang, + [], + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = self.name + yield task + + if not kw["create_monthly_archive"]: + continue # Just to avoid nesting the other loop in this if + template_name = "list_post.tmpl" + for yearmonth, posts in self.site.posts_per_month.items(): + output_name = os.path.join( + kw['output_folder'], self.site.path("archive", yearmonth, + lang)) + year, month = yearmonth.split('/') + post_list = [self.site.global_data[post] for post in posts] + post_list.sort(key=lambda a: a.date) + post_list.reverse() + context = {} + context["lang"] = lang + context["posts"] = post_list + context["permalink"] = self.site.link("archive", year, lang) + + context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format( + year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang)) + task = self.site.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = self.name + yield task + + if not kw['create_single_archive']: + # And an "all your years" page for yearly and monthly archives + years = list(self.site.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.site.path("archive", None, + lang)) + context["title"] = kw["messages"][lang]["Archive"] + context["items"] = [(year, self.site.link("archive", year, lang)) + for year in years] + context["permalink"] = self.site.link("archive", None, lang) + task = self.site.generic_post_list_renderer( + lang, + [], + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = self.name + yield task + + def archive_path(self, name, lang): + if name: + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['ARCHIVE_PATH'], name, + self.site.config['INDEX_FILE']] if _f] + else: + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['ARCHIVE_PATH'], + self.site.config['ARCHIVE_FILENAME']] if _f] diff --git a/nikola/plugins/task/build_less.plugin b/nikola/plugins/task/build_less.plugin new file mode 100644 index 0000000..27ca8cd --- /dev/null +++ b/nikola/plugins/task/build_less.plugin @@ -0,0 +1,10 @@ +[Core] +Name = build_less +Module = build_less + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Build CSS out of LESS sources + diff --git a/nikola/plugins/task/build_less.py b/nikola/plugins/task/build_less.py new file mode 100644 index 0000000..8889cbe --- /dev/null +++ b/nikola/plugins/task/build_less.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals + +import codecs +import glob +import os +import subprocess + +from nikola.plugin_categories import Task +from nikola import utils + + +class BuildLess(Task): + """Generate CSS out of LESS sources.""" + + name = "build_less" + sources_folder = "less" + sources_ext = ".less" + compiler_name = "lessc" + + def gen_tasks(self): + """Generate CSS out of LESS sources.""" + + kw = { + 'cache_folder': self.site.config['CACHE_FOLDER'], + 'themes': self.site.THEMES, + } + + # Find where in the theme chain we define the LESS targets + # There can be many *.less in the folder, but we only will build + # the ones listed in less/targets + targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) + try: + with codecs.open(targets_path, "rb", "utf-8") as inf: + targets = [x.strip() for x in inf.readlines()] + except Exception: + targets = [] + + for theme_name in kw['themes']: + src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) + for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): + task['basename'] = 'prepare_less_sources' + yield task + + # Build targets and write CSS files + base_path = utils.get_theme_path(self.site.THEMES[0]) + dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') + # Make everything depend on all sources, rough but enough + deps = glob.glob(os.path.join( + base_path, + self.sources_folder, + "*{0}".format(self.sources_ext))) + + def compile_target(target, dst): + utils.makedirs(dst_dir) + src = os.path.join(kw['cache_folder'], self.sources_folder, target) + compiled = subprocess.check_output([self.compiler_name, src]) + with open(dst, "wb+") as outf: + outf.write(compiled) + + yield self.group_task() + + for target in targets: + dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) + yield { + 'basename': self.name, + 'name': dst, + 'targets': [dst], + 'file_dep': deps, + 'task_dep': ['prepare_less_sources'], + 'actions': ((compile_target, [target, dst]), ), + 'uptodate': [utils.config_changed(kw)], + 'clean': True + } diff --git a/nikola/plugins/task/build_sass.plugin b/nikola/plugins/task/build_sass.plugin new file mode 100644 index 0000000..746c1df --- /dev/null +++ b/nikola/plugins/task/build_sass.plugin @@ -0,0 +1,9 @@ +[Core] +Name = build_sass +Module = build_sass + +[Documentation] +Author = Roberto Alsina, Chris “Kwpolska” Warrick +Version = 0.1 +Website = http://getnikola.com +Description = Build CSS out of Sass sources diff --git a/nikola/plugins/task/build_sass.py b/nikola/plugins/task/build_sass.py new file mode 100644 index 0000000..a5d22fb --- /dev/null +++ b/nikola/plugins/task/build_sass.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals + +import codecs +import glob +import os +import subprocess + +from nikola.plugin_categories import Task +from nikola import utils + + +class BuildSass(Task): + """Generate CSS out of Sass sources.""" + + name = "build_sass" + sources_folder = "sass" + sources_ext = (".sass", ".scss") + compiler_name = "sass" + + def gen_tasks(self): + """Generate CSS out of Sass sources.""" + self.logger = utils.get_logger('build_sass', self.site.loghandlers) + + kw = { + 'cache_folder': self.site.config['CACHE_FOLDER'], + 'themes': self.site.THEMES, + } + + # Find where in the theme chain we define the Sass targets + # There can be many *.sass/*.scss in the folder, but we only + # will build the ones listed in sass/targets + targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) + try: + with codecs.open(targets_path, "rb", "utf-8") as inf: + targets = [x.strip() for x in inf.readlines()] + except Exception: + targets = [] + + for theme_name in kw['themes']: + src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) + for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): + task['basename'] = 'prepare_sass_sources' + yield task + + # Build targets and write CSS files + base_path = utils.get_theme_path(self.site.THEMES[0]) + dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') + # Make everything depend on all sources, rough but enough + deps = glob.glob(os.path.join( + base_path, + self.sources_folder, + *("*{0}".format(ext) for ext in self.sources_ext))) + + def compile_target(target, dst): + utils.makedirs(dst_dir) + src = os.path.join(kw['cache_folder'], self.sources_folder, target) + compiled = subprocess.check_output([self.compiler_name, src]) + with open(dst, "wb+") as outf: + outf.write(compiled) + + yield self.group_task() + + # We can have file conflicts. This is a way to prevent them. + # I orignally wanted to use sets and their cannot-have-duplicates + # magic, but I decided not to do this so we can show the user + # what files were problematic. + # If we didn’t do this, there would be a cryptic message from doit + # instead. + seennames = {} + for target in targets: + base = os.path.splitext(target)[0] + dst = os.path.join(dst_dir, base + ".css") + + if base in seennames: + self.logger.error( + 'Duplicate filenames for SASS compiled files: {0} and ' + '{1} (both compile to {2})'.format( + seennames[base], target, base + ".css")) + else: + seennames.update({base: target}) + + yield { + 'basename': self.name, + 'name': dst, + 'targets': [dst], + 'file_dep': deps, + 'task_dep': ['prepare_sass_sources'], + 'actions': ((compile_target, [target, dst]), ), + 'uptodate': [utils.config_changed(kw)], + 'clean': True + } diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin new file mode 100644 index 0000000..e0b0a4d --- /dev/null +++ b/nikola/plugins/task/bundles.plugin @@ -0,0 +1,10 @@ +[Core] +Name = create_bundles +Module = bundles + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Theme bundles using WebAssets + diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py new file mode 100644 index 0000000..488f96f --- /dev/null +++ b/nikola/plugins/task/bundles.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals + +import os + +try: + import webassets +except ImportError: + webassets = None # NOQA + +from nikola.plugin_categories import LateTask +from nikola import utils + + +class BuildBundles(LateTask): + """Bundle assets using WebAssets.""" + + name = "create_bundles" + + def set_site(self, site): + super(BuildBundles, self).set_site(site) + if webassets is None: + self.site.config['USE_BUNDLES'] = False + + def gen_tasks(self): + """Bundle assets using WebAssets.""" + + kw = { + 'filters': self.site.config['FILTERS'], + 'output_folder': self.site.config['OUTPUT_FOLDER'], + 'cache_folder': self.site.config['CACHE_FOLDER'], + 'theme_bundles': get_theme_bundles(self.site.THEMES), + 'themes': self.site.THEMES, + 'files_folders': self.site.config['FILES_FOLDERS'], + 'code_color_scheme': self.site.config['CODE_COLOR_SCHEME'], + } + + def build_bundle(output, inputs): + out_dir = os.path.join(kw['output_folder'], + os.path.dirname(output)) + inputs = [i for i in inputs if os.path.isfile( + os.path.join(out_dir, i))] + cache_dir = os.path.join(kw['cache_folder'], 'webassets') + utils.makedirs(cache_dir) + env = webassets.Environment(out_dir, os.path.dirname(output), + cache=cache_dir) + if inputs: + bundle = webassets.Bundle(*inputs, output=os.path.basename(output)) + env.register(output, bundle) + # This generates the file + env[output].urls() + else: + with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'): + pass # Create empty file + + yield self.group_task() + if (webassets is not None and self.site.config['USE_BUNDLES'] is not + 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(kw['output_folder'], dname, fname) + for fname in files] + file_dep = filter(os.path.isfile, file_dep) # removes missing files + task = { + 'file_dep': list(file_dep), + 'task_dep': ['copy_assets'], + 'basename': str(self.name), + 'name': str(output_path), + 'actions': [(build_bundle, (name, files))], + 'targets': [output_path], + 'uptodate': [utils.config_changed(kw)], + 'clean': True, + } + yield utils.apply_filters(task, kw['filters']) + + +def get_theme_bundles(themes): + """Given a theme chain, return the bundle definitions.""" + bundles = {} + for theme_name in themes: + bundles_path = os.path.join( + utils.get_theme_path(theme_name), 'bundles') + if os.path.isfile(bundles_path): + with open(bundles_path) as fd: + for line in fd: + name, files = line.split('=') + files = [f.strip() for f in files.split(',')] + bundles[name.strip()] = files + break + return bundles diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin new file mode 100644 index 0000000..28b9e32 --- /dev/null +++ b/nikola/plugins/task/copy_assets.plugin @@ -0,0 +1,10 @@ +[Core] +Name = copy_assets +Module = copy_assets + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Copy theme assets into output. + diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py new file mode 100644 index 0000000..f3d85df --- /dev/null +++ b/nikola/plugins/task/copy_assets.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import codecs +import os + +from nikola.plugin_categories import Task +from nikola import utils + + +class CopyAssets(Task): + """Copy theme assets into output.""" + + name = "copy_assets" + + def gen_tasks(self): + """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. + """ + + kw = { + "themes": self.site.THEMES, + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], + } + has_code_css = False + tasks = {} + code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') + + yield self.group_task() + + 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 + if task['targets'][0] == code_css_path: + has_code_css = True + tasks[task['name']] = task + task['uptodate'] = [utils.config_changed(kw)] + task['basename'] = self.name + yield utils.apply_filters(task, kw['filters']) + + if not has_code_css: # Generate it + + def create_code_css(): + from pygments.formatters import get_formatter_by_name + formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) + utils.makedirs(os.path.dirname(code_css_path)) + with codecs.open(code_css_path, 'wb+', 'utf8') as outf: + outf.write(formatter.get_style_defs('.code')) + outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}") + + task = { + 'basename': self.name, + 'name': code_css_path, + 'targets': [code_css_path], + 'uptodate': [utils.config_changed(kw)], + 'actions': [(create_code_css, [])], + 'clean': True, + } + yield utils.apply_filters(task, kw['filters']) diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin new file mode 100644 index 0000000..45c9e0d --- /dev/null +++ b/nikola/plugins/task/copy_files.plugin @@ -0,0 +1,10 @@ +[Core] +Name = copy_files +Module = copy_files + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Copy static files into the output. + diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py new file mode 100644 index 0000000..88e89eb --- /dev/null +++ b/nikola/plugins/task/copy_files.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import os + +from nikola.plugin_categories import Task +from nikola import utils + + +class CopyFiles(Task): + """Copy static files into the output folder.""" + + name = "copy_files" + + def gen_tasks(self): + """Copy static files into the output folder.""" + + kw = { + 'files_folders': self.site.config['FILES_FOLDERS'], + 'output_folder': self.site.config['OUTPUT_FOLDER'], + 'filters': self.site.config['FILTERS'], + } + + yield self.group_task() + 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): + task['basename'] = self.name + task['uptodate'] = [utils.config_changed(kw)] + yield utils.apply_filters(task, filters) diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin new file mode 100644 index 0000000..8352151 --- /dev/null +++ b/nikola/plugins/task/galleries.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_galleries +Module = galleries + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create image galleries automatically. + diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py new file mode 100644 index 0000000..cf670e0 --- /dev/null +++ b/nikola/plugins/task/galleries.py @@ -0,0 +1,553 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +import codecs +import datetime +import glob +import json +import mimetypes +import os +try: + from urlparse import urljoin +except ImportError: + from urllib.parse import urljoin # NOQA + +Image = None +try: + from PIL import Image, ExifTags # NOQA +except ImportError: + try: + import Image as _Image + import ExifTags + Image = _Image + except ImportError: + pass +import PyRSS2Gen as rss + +from nikola.plugin_categories import Task +from nikola import utils +from nikola.post import Post + + +class Galleries(Task): + """Render image galleries.""" + + name = 'render_galleries' + dates = {} + + def set_site(self, site): + site.register_path_handler('gallery', self.gallery_path) + site.register_path_handler('gallery_rss', self.gallery_rss_path) + return super(Galleries, self).set_site(site) + + def gallery_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['GALLERY_PATH'], name, + self.site.config['INDEX_FILE']] if _f] + + def gallery_rss_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['GALLERY_PATH'], name, + 'rss.xml'] if _f] + + def gen_tasks(self): + """Render image galleries.""" + + self.logger = utils.get_logger('render_galleries', self.site.loghandlers) + self.image_ext_list = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff'] + self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', [])) + + self.kw = { + 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'], + 'max_image_size': self.site.config['MAX_IMAGE_SIZE'], + 'output_folder': self.site.config['OUTPUT_FOLDER'], + 'cache_folder': self.site.config['CACHE_FOLDER'], + 'default_lang': self.site.config['DEFAULT_LANG'], + 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'], + 'gallery_path': self.site.config['GALLERY_PATH'], + 'sort_by_date': self.site.config['GALLERY_SORT_BY_DATE'], + 'filters': self.site.config['FILTERS'], + 'translations': self.site.config['TRANSLATIONS'], + 'global_context': self.site.GLOBAL_CONTEXT, + "feed_length": self.site.config['FEED_LENGTH'], + } + + yield self.group_task() + + template_name = "gallery.tmpl" + + # Find all galleries we need to process + self.find_galleries() + + # Create all output folders + for task in self.create_galleries(): + yield task + + # For each gallery: + for gallery in self.gallery_list: + + # Create subfolder list + folder_list = [x.split(os.sep)[-2] for x in + glob.glob(os.path.join(gallery, '*') + os.sep)] + + # Parse index into a post (with translations) + post = self.parse_index(gallery) + + # Create image list, filter exclusions + image_list = self.get_image_list(gallery) + + # Sort as needed + # Sort by date + if self.kw['sort_by_date']: + image_list.sort(key=lambda a: self.image_date(a)) + else: # Sort by name + image_list.sort() + + # Create thumbnails and large images in destination + for image in image_list: + for task in self.create_target_images(image): + yield task + + # Remove excluded images + for image in self.get_excluded_images(gallery): + for task in self.remove_excluded_image(image): + yield task + + crumbs = utils.get_crumbs(gallery) + + # Create index.html for each language + for lang in self.kw['translations']: + dst = os.path.join( + self.kw['output_folder'], + self.site.path( + "gallery", + os.path.relpath(gallery, self.kw['gallery_path']), lang)) + dst = os.path.normpath(dst) + + context = {} + context["lang"] = lang + if post: + context["title"] = post.title(lang) + else: + context["title"] = os.path.basename(gallery) + context["description"] = None + + image_name_list = [os.path.basename(p) for p in image_list] + + if self.kw['use_filename_as_title']: + img_titles = [] + for fn in image_name_list: + name_without_ext = os.path.splitext(fn)[0] + img_titles.append( + 'id="{0}" alt="{1}" title="{2}"'.format( + name_without_ext, + name_without_ext, + utils.unslugify(name_without_ext))) + else: + img_titles = [''] * len(image_name_list) + + thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list] + thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs] + + ## TODO: in v7 remove images from context, use photo_array + context["images"] = list(zip(image_name_list, thumbs, img_titles)) + context["folders"] = folder_list + context["crumbs"] = crumbs + context["permalink"] = self.site.link( + "gallery", os.path.basename(gallery), lang) + # FIXME: use kw + context["enable_comments"] = ( + self.site.config["COMMENTS_IN_GALLERIES"]) + context["thumbnail_size"] = self.kw["thumbnail_size"] + + # FIXME: render post in a task + if post: + post.compile(lang) + context['text'] = post.text(lang) + else: + context['text'] = '' + + file_dep = self.site.template_system.template_deps( + template_name) + image_list + thumbs + + yield utils.apply_filters({ + 'basename': self.name, + 'name': dst, + 'file_dep': file_dep, + 'targets': [dst], + 'actions': [ + (self.render_gallery_index, ( + template_name, + dst, + context, + image_list, + thumbs, + file_dep))], + 'clean': True, + 'uptodate': [utils.config_changed({ + 1: self.kw, + 2: self.site.config["COMMENTS_IN_GALLERIES"], + 3: context, + })], + }, self.kw['filters']) + + # RSS for the gallery + rss_dst = os.path.join( + self.kw['output_folder'], + self.site.path( + "gallery_rss", + os.path.relpath(gallery, self.kw['gallery_path']), lang)) + rss_dst = os.path.normpath(rss_dst) + + yield utils.apply_filters({ + 'basename': self.name, + 'name': rss_dst, + 'file_dep': file_dep, + 'targets': [rss_dst], + 'actions': [ + (self.gallery_rss, ( + image_list, + img_titles, + lang, + self.site.link( + "gallery_rss", os.path.basename(gallery), lang), + rss_dst, + context['title'] + ))], + 'clean': True, + 'uptodate': [utils.config_changed({ + 1: self.kw, + })], + }, self.kw['filters']) + + def find_galleries(self): + """Find all galleries to be processed according to conf.py""" + + self.gallery_list = [] + for root, dirs, files in os.walk(self.kw['gallery_path']): + self.gallery_list.append(root) + + def create_galleries(self): + """Given a list of galleries, create the output folders.""" + + # gallery_path is "gallery/foo/name" + for gallery_path in self.gallery_list: + gallery_name = os.path.relpath(gallery_path, self.kw['gallery_path']) + # have to use dirname because site.path returns .../index.html + output_gallery = os.path.dirname( + os.path.join( + self.kw["output_folder"], + self.site.path("gallery", gallery_name))) + output_gallery = os.path.normpath(output_gallery) + # Task to create gallery in output/ + yield { + 'basename': self.name, + 'name': output_gallery, + 'actions': [(utils.makedirs, (output_gallery,))], + 'targets': [output_gallery], + 'clean': True, + 'uptodate': [utils.config_changed(self.kw)], + } + + def parse_index(self, gallery): + """Returns a Post object if there is an index.txt.""" + + index_path = os.path.join(gallery, "index.txt") + destination = os.path.join( + self.kw["output_folder"], + gallery) + if os.path.isfile(index_path): + post = Post( + index_path, + self.site.config, + destination, + False, + self.site.MESSAGES, + 'story.tmpl', + self.site.get_compiler(index_path).compile_html + ) + else: + post = None + return post + + def get_excluded_images(self, gallery_path): + 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 = ["{0}/{1}".format(gallery_path, i) for i in excluded_image_name_list] + return excluded_image_list + + def get_image_list(self, gallery_path): + + # Gather image_list contains "gallery/name/image_name.jpg" + image_list = [] + + for ext in self.image_ext_list: + image_list += glob.glob(gallery_path + '/*' + ext.lower()) +\ + glob.glob(gallery_path + '/*' + ext.upper()) + + # Filter ignored images + excluded_image_list = self.get_excluded_images(gallery_path) + image_set = set(image_list) - set(excluded_image_list) + image_list = list(image_set) + return image_list + + def create_target_images(self, img): + gallery_name = os.path.relpath(os.path.dirname(img), self.kw['gallery_path']) + output_gallery = os.path.dirname( + os.path.join( + self.kw["output_folder"], + self.site.path("gallery", gallery_name))) + # Do thumbnails and copy originals + # img is "galleries/name/image_name.jpg" + # img_name is "image_name.jpg" + # fname, ext are "image_name", ".jpg" + # thumb_path is + # "output/GALLERY_PATH/name/image_name.thumbnail.jpg" + img_name = os.path.basename(img) + fname, ext = os.path.splitext(img_name) + thumb_path = os.path.join( + output_gallery, + ".thumbnail".join([fname, ext])) + # thumb_path is "output/GALLERY_PATH/name/image_name.jpg" + orig_dest_path = os.path.join(output_gallery, img_name) + yield utils.apply_filters({ + 'basename': self.name, + 'name': thumb_path, + 'file_dep': [img], + 'targets': [thumb_path], + 'actions': [ + (self.resize_image, + (img, thumb_path, self.kw['thumbnail_size'])) + ], + 'clean': True, + 'uptodate': [utils.config_changed({ + 1: self.kw['thumbnail_size'] + })], + }, self.kw['filters']) + + yield utils.apply_filters({ + 'basename': self.name, + 'name': orig_dest_path, + 'file_dep': [img], + 'targets': [orig_dest_path], + 'actions': [ + (self.resize_image, + (img, orig_dest_path, self.kw['max_image_size'])) + ], + 'clean': True, + 'uptodate': [utils.config_changed({ + 1: self.kw['max_image_size'] + })], + }, self.kw['filters']) + + def remove_excluded_image(self, img): + # Remove excluded images + # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path + # and we should remove both the large and thumbnail *destination* paths + + img = os.path.relpath(img, self.kw['gallery_path']) + output_folder = os.path.dirname( + os.path.join( + self.kw["output_folder"], + self.site.path("gallery", os.path.dirname(img)))) + img_path = os.path.join(output_folder, os.path.basename(img)) + fname, ext = os.path.splitext(img_path) + thumb_path = fname + '.thumbnail' + ext + + yield utils.apply_filters({ + 'basename': '_render_galleries_clean', + 'name': thumb_path, + 'actions': [ + (utils.remove_file, (thumb_path,)) + ], + 'clean': True, + 'uptodate': [utils.config_changed(self.kw)], + }, self.kw['filters']) + + yield utils.apply_filters({ + 'basename': '_render_galleries_clean', + 'name': img_path, + 'actions': [ + (utils.remove_file, (img_path,)) + ], + 'clean': True, + 'uptodate': [utils.config_changed(self.kw)], + }, self.kw['filters']) + + def render_gallery_index( + self, + template_name, + output_name, + context, + img_list, + thumbs, + file_dep): + """Build the gallery index.""" + + # The photo array needs to be created here, because + # it relies on thumbnails already being created on + # output + + def url_from_path(p): + url = '/'.join(os.path.relpath(p, os.path.dirname(output_name) + os.sep).split(os.sep)) + return url + + photo_array = [] + for img, thumb in zip(img_list, thumbs): + im = Image.open(thumb) + w, h = im.size + title = '' + if self.kw['use_filename_as_title']: + title = utils.unslugify(os.path.splitext(img)[0]) + # Thumbs are files in output, we need URLs + photo_array.append({ + 'url': url_from_path(img), + 'url_thumb': url_from_path(thumb), + 'title': title, + 'size': { + 'w': w, + 'h': h + }, + }) + context['photo_array_json'] = json.dumps(photo_array) + context['photo_array'] = photo_array + + self.site.render_template(template_name, output_name, context) + + def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title): + """Create a RSS showing the latest images in the gallery. + + This doesn't use generic_rss_renderer because it + doesn't involve Post objects. + """ + + def make_url(url): + return urljoin(self.site.config['BASE_URL'], url) + + items = [] + for img, full_title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]: + img_size = os.stat( + os.path.join( + self.site.config['OUTPUT_FOLDER'], img)).st_size + args = { + 'title': full_title.split('"')[-2], + 'link': make_url(img), + 'guid': rss.Guid(img, False), + 'pubDate': self.image_date(img), + 'enclosure': rss.Enclosure( + make_url(img), + img_size, + mimetypes.guess_type(img)[0] + ), + } + items.append(rss.RSSItem(**args)) + rss_obj = utils.ExtendedRSS2( + title=title, + link=make_url(permalink), + description='', + lastBuildDate=datetime.datetime.now(), + items=items, + generator='nikola', + language=lang + ) + rss_obj.self_url = make_url(permalink) + rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" + dst_dir = os.path.dirname(output_path) + utils.makedirs(dst_dir) + with codecs.open(output_path, "wb+", "utf-8") as rss_file: + data = rss_obj.to_xml(encoding='utf-8') + if isinstance(data, utils.bytes_str): + data = data.decode('utf-8') + rss_file.write(data) + + def resize_image(self, src, dst, max_size): + """Make a copy of the image in the requested size.""" + if not Image: + utils.copy_file(src, dst) + return + im = Image.open(src) + w, h = im.size + if w > max_size or h > max_size: + size = max_size, max_size + + # Panoramas get larger thumbnails because they look *awful* + if w > 2 * h: + size = min(w, max_size * 4), min(w, max_size * 4) + + try: + exif = im._getexif() + except Exception: + exif = None + if exif is not None: + for tag, value in list(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 + try: + im.thumbnail(size, Image.ANTIALIAS) + im.save(dst) + except Exception: + self.logger.warn("Can't thumbnail {0}, using original image as thumbnail".format(src)) + utils.copy_file(src, dst) + else: # Image is small + utils.copy_file(src, dst) + + def image_date(self, src): + """Try to figure out the date of the image.""" + if src not in self.dates: + try: + im = Image.open(src) + exif = im._getexif() + except Exception: + exif = None + if exif is not None: + for tag, value in list(exif.items()): + decoded = ExifTags.TAGS.get(tag, tag) + if decoded == 'DateTimeOriginal': + try: + self.dates[src] = datetime.datetime.strptime( + value, r'%Y:%m:%d %H:%M:%S') + break + except ValueError: # Invalid EXIF date. + pass + if src not in self.dates: + self.dates[src] = datetime.datetime.fromtimestamp( + os.stat(src).st_mtime) + return self.dates[src] diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin new file mode 100644 index 0000000..b68ea6f --- /dev/null +++ b/nikola/plugins/task/gzip.plugin @@ -0,0 +1,10 @@ +[Core] +Name = gzip +Module = gzip + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create gzipped copies of files + diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py new file mode 100644 index 0000000..738d52c --- /dev/null +++ b/nikola/plugins/task/gzip.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Create gzipped copies of files.""" + +import gzip +import os +import shlex +import subprocess + +from nikola.plugin_categories import TaskMultiplier + + +class GzipFiles(TaskMultiplier): + """If appropiate, create tasks to create gzipped versions of files.""" + + name = "gzip" + is_default = True + + def process(self, task, prefix): + if not self.site.config['GZIP_FILES']: + return [] + if task.get('name') is None: + return [] + gzip_task = { + 'file_dep': [], + 'targets': [], + 'actions': [], + 'basename': '{0}_gzip'.format(prefix), + 'name': task.get('name').split(":", 1)[-1] + '.gz', + 'clean': True, + } + targets = task.get('targets', []) + flag = False + for target in targets: + ext = os.path.splitext(target)[1] + if (ext.lower() in self.site.config['GZIP_EXTENSIONS'] and + target.startswith(self.site.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, self.site.config['GZIP_COMMAND']))) + if not flag: + return [] + return [gzip_task] + + +def create_gzipped_copy(in_path, out_path, command=None): + if command: + subprocess.check_call(shlex.split(command.format(filename=in_path))) + else: + with gzip.GzipFile(out_path, 'wb+') as outf: + with open(in_path, 'rb') as inf: + outf.write(inf.read()) diff --git a/nikola/plugins/task/indexes.plugin b/nikola/plugins/task/indexes.plugin new file mode 100644 index 0000000..a18942c --- /dev/null +++ b/nikola/plugins/task/indexes.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_indexes +Module = indexes + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Generates the blog's index pages. + diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py new file mode 100644 index 0000000..0d20422 --- /dev/null +++ b/nikola/plugins/task/indexes.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +import glob +import itertools +import os + +from nikola.plugin_categories import Task +from nikola.utils import config_changed + + +class Indexes(Task): + """Render the blog indexes.""" + + name = "render_indexes" + + def set_site(self, site): + site.register_path_handler('index', self.index_path) + return super(Indexes, self).set_site(site) + + def gen_tasks(self): + self.site.scan_posts() + yield self.group_task() + + kw = { + "translations": self.site.config['TRANSLATIONS'], + "index_display_post_count": + self.site.config['INDEX_DISPLAY_POST_COUNT'], + "messages": self.site.MESSAGES, + "index_teasers": self.site.config['INDEX_TEASERS'], + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "indexes_title": self.site.config['INDEXES_TITLE'], + "indexes_pages": self.site.config['INDEXES_PAGES'], + "blog_title": self.site.config["BLOG_TITLE"], + } + + template_name = "index.tmpl" + posts = [x for x in self.site.timeline if x.use_in_feeds] + for lang in kw["translations"]: + # Split in smaller lists + lists = [] + if kw["hide_untranslated_posts"]: + filtered_posts = [x for x in posts if x.is_translation_available(lang)] + else: + filtered_posts = posts + lists.append(filtered_posts[:kw["index_display_post_count"]]) + filtered_posts = filtered_posts[kw["index_display_post_count"]:] + while filtered_posts: + lists.append(filtered_posts[-kw["index_display_post_count"]:]) + filtered_posts = filtered_posts[:-kw["index_display_post_count"]] + num_pages = len(lists) + for i, post_list in enumerate(lists): + context = {} + indexes_title = kw['indexes_title'] or kw['blog_title'] + if kw["indexes_pages"]: + indexes_pages = kw["indexes_pages"] % i + else: + indexes_pages = " (" + \ + kw["messages"][lang]["old posts page %d"] % i + ")" + if i > 0: + context["title"] = indexes_title + indexes_pages + else: + context["title"] = indexes_title + context["prevlink"] = None + context["nextlink"] = None + context['index_teasers'] = kw['index_teasers'] + if i == 0: # index.html page + context["prevlink"] = None + if num_pages > 1: + context["nextlink"] = "index-{0}.html".format(num_pages - 1) + else: + context["nextlink"] = None + else: # index-x.html pages + if i > 1: + context["nextlink"] = "index-{0}.html".format(i - 1) + if i < num_pages - 1: + context["prevlink"] = "index-{0}.html".format(i + 1) + elif i == num_pages - 1: + context["prevlink"] = "index.html" + context["permalink"] = self.site.link("index", i, lang) + output_name = os.path.join( + kw['output_folder'], self.site.path("index", i, + lang)) + task = self.site.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = 'render_indexes' + yield task + + if not self.site.config["STORY_INDEX"]: + return + kw = { + "translations": self.site.config['TRANSLATIONS'], + "post_pages": self.site.config["post_pages"], + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + } + template_name = "list.tmpl" + for lang in kw["translations"]: + # Need to group by folder to avoid duplicated tasks (Issue #758) + for dirname, wildcards in itertools.groupby((w for w, d, x, i in kw["post_pages"] if not i), os.path.dirname): + context = {} + # vim/pyflakes thinks it's unused + # src_dir = os.path.dirname(wildcard) + files = [] + for wildcard in wildcards: + files += glob.glob(wildcard) + post_list = [self.site.global_data[p] for p in files] + output_name = os.path.join(kw["output_folder"], + self.site.path("post_path", + wildcard, + lang)).encode('utf8') + context["items"] = [(post.title(lang), post.permalink(lang)) + for post in post_list] + task = self.site.generic_post_list_renderer(lang, post_list, + output_name, + template_name, + kw['filters'], + context) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = self.name + yield task + + def index_path(self, name, lang): + if name not in [None, 0]: + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['INDEX_PATH'], + 'index-{0}.html'.format(name)] if _f] + else: + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['INDEX_PATH'], + self.site.config['INDEX_FILE']] + if _f] diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin new file mode 100644 index 0000000..c93184d --- /dev/null +++ b/nikola/plugins/task/listings.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_listings +Module = listings + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Render code listings into output + diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py new file mode 100644 index 0000000..ab62e74 --- /dev/null +++ b/nikola/plugins/task/listings.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals, print_function + +import os + +from pygments import highlight +from pygments.lexers import get_lexer_for_filename, TextLexer +from pygments.formatters import HtmlFormatter + +from nikola.plugin_categories import Task +from nikola import utils + + +class Listings(Task): + """Render pretty listings.""" + + name = "render_listings" + + def set_site(self, site): + site.register_path_handler('listing', self.listing_path) + return super(Listings, self).set_site(site) + + def gen_tasks(self): + """Render pretty code listings.""" + kw = { + "default_lang": self.site.config["DEFAULT_LANG"], + "listings_folder": self.site.config["LISTINGS_FOLDER"], + "output_folder": self.site.config["OUTPUT_FOLDER"], + "index_file": self.site.config["INDEX_FILE"], + } + + # Things to ignore in listings + ignored_extensions = (".pyc",) + + def render_listing(in_name, out_name, folders=[], files=[]): + if in_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(in_name), + anchorlinenos=True)) + title = os.path.basename(in_name) + else: + code = '' + title = '' + crumbs = utils.get_crumbs(os.path.relpath(out_name, + kw['output_folder']), + is_file=True) + context = { + 'code': code, + 'title': title, + 'crumbs': crumbs, + 'lang': kw['default_lang'], + 'folders': folders, + 'files': files, + 'description': title, + } + self.site.render_template('listing.tmpl', out_name, + context) + + yield self.group_task() + + template_deps = self.site.template_system.template_deps('listing.tmpl') + for root, dirs, files in os.walk(kw['listings_folder']): + # Render all files + out_name = os.path.join( + kw['output_folder'], + root, kw['index_file'] + ) + yield { + 'basename': self.name, + 'name': out_name, + 'file_dep': template_deps, + 'targets': [out_name], + 'actions': [(render_listing, [None, out_name, dirs, files])], + # This is necessary to reflect changes in blog title, + # sidebar links, etc. + 'uptodate': [utils.config_changed( + self.site.GLOBAL_CONTEXT)], + 'clean': True, + } + for f in files: + ext = os.path.splitext(f)[-1] + if ext in ignored_extensions: + continue + in_name = os.path.join(root, f) + out_name = os.path.join( + kw['output_folder'], + root, + f) + '.html' + yield { + 'basename': self.name, + 'name': out_name, + 'file_dep': template_deps + [in_name], + 'targets': [out_name], + 'actions': [(render_listing, [in_name, out_name])], + # This is necessary to reflect changes in blog title, + # sidebar links, etc. + 'uptodate': [utils.config_changed( + self.site.GLOBAL_CONTEXT)], + 'clean': True, + } + + def listing_path(self, name, lang): + return [_f for _f in [self.site.config['LISTINGS_FOLDER'], name + + '.html'] if _f] diff --git a/nikola/plugins/task/localsearch.plugin b/nikola/plugins/task/localsearch.plugin new file mode 100644 index 0000000..86accb6 --- /dev/null +++ b/nikola/plugins/task/localsearch.plugin @@ -0,0 +1,10 @@ +[Core] +Name = local_search +Module = localsearch + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create data files for local search via Tipue + diff --git a/nikola/plugins/task/localsearch/MIT-LICENSE.txt b/nikola/plugins/task/localsearch/MIT-LICENSE.txt new file mode 100644 index 0000000..f131068 --- /dev/null +++ b/nikola/plugins/task/localsearch/MIT-LICENSE.txt @@ -0,0 +1,20 @@ +Tipue Search Copyright (c) 2012 Tipue + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/nikola/plugins/task/localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py new file mode 100644 index 0000000..9162604 --- /dev/null +++ b/nikola/plugins/task/localsearch/__init__.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +import codecs +import json +import os + +from doit.tools import result_dep + +from nikola.plugin_categories import LateTask +from nikola.utils import config_changed, copy_tree, makedirs + +# This is what we need to produce: +#var tipuesearch = {"pages": [ + #{"title": "Tipue Search, a jQuery site search engine", "text": "Tipue + #Search is a site search engine jQuery plugin. It's free for both commercial and + #non-commercial use and released under the MIT License. Tipue Search includes + #features such as word stemming and word replacement.", "tags": "JavaScript", + #"loc": "http://www.tipue.com/search"}, + #{"title": "Tipue Search demo", "text": "Tipue Search demo. Tipue Search is + #a site search engine jQuery plugin.", "tags": "JavaScript", "loc": + #"http://www.tipue.com/search/demo"}, + #{"title": "About Tipue", "text": "Tipue is a small web development/design + #studio based in North London. We've been around for over a decade.", "tags": "", + #"loc": "http://www.tipue.com/about"} +#]}; + + +class Tipue(LateTask): + """Render the blog posts as JSON data.""" + + name = "local_search" + + def gen_tasks(self): + self.site.scan_posts() + + kw = { + "translations": self.site.config['TRANSLATIONS'], + "output_folder": self.site.config['OUTPUT_FOLDER'], + } + + posts = self.site.timeline[:] + dst_path = os.path.join(kw["output_folder"], "assets", "js", + "tipuesearch_content.json") + + def save_data(): + pages = [] + for lang in kw["translations"]: + for post in posts: + # Don't index drafts (Issue #387) + if post.is_draft or post.is_retired or post.publish_later: + continue + text = post.text(lang, strip_html=True) + text = text.replace('^', '') + + data = {} + data["title"] = post.title(lang) + data["text"] = text + data["tags"] = ",".join(post.tags) + data["loc"] = post.permalink(lang) + pages.append(data) + output = json.dumps({"pages": pages}, indent=2) + makedirs(os.path.dirname(dst_path)) + with codecs.open(dst_path, "wb+", "utf8") as fd: + fd.write(output) + + yield { + "basename": str(self.name), + "name": dst_path, + "targets": [dst_path], + "actions": [(save_data, [])], + 'uptodate': [config_changed(kw), result_dep('sitemap')] + } + # Note: The task should run everytime a new file is added or a + # file is changed. We cheat, and depend on the sitemap task, + # to run everytime a new file is added. + + # Copy all the assets to the right places + asset_folder = os.path.join(os.path.dirname(__file__), "files") + for task in copy_tree(asset_folder, kw["output_folder"]): + task["basename"] = str(self.name) + yield task diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif Binary files differnew file mode 100644 index 0000000..9c97738 --- /dev/null +++ b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/search.png b/nikola/plugins/task/localsearch/files/assets/css/img/search.png Binary files differnew file mode 100755 index 0000000..9ab0f2c --- /dev/null +++ b/nikola/plugins/task/localsearch/files/assets/css/img/search.png diff --git a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css new file mode 100755 index 0000000..2230193 --- /dev/null +++ b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css @@ -0,0 +1,159 @@ + +/* +Tipue Search 3.0.1 +Copyright (c) 2013 Tipue +Tipue Search is released under the MIT License +http://www.tipue.com/search +*/ + + +#tipue_search_input +{ + font: 12px/1.7 'open sans', sans-serif; + color: #333; + padding: 7px; + width: 150px; + border: 1px solid #e2e2e2; + border-radius: 0; + -moz-appearance: none; + -webkit-appearance: none; + box-shadow: none; + outline: 0; + margin: 0; +} +#tipue_search_input:focus +{ + border: 1px solid #ccc; +} +#tipue_search_button +{ + width: 70px; + height: 36px; + border: 0; + border-radius: 1px; + background: #5193fb url('img/search.png') no-repeat center; + outline: none; +} +#tipue_search_button:hover +{ + background-color: #4589fb; +} + +#tipue_search_content +{ + clear: left; + max-width: 650px; + padding: 25px 0 13px 0; + margin: 0; +} +#tipue_search_loading +{ + padding-top: 60px; + background: #fff url('img/loader.gif') no-repeat left; +} + +#tipue_search_warning_head +{ + font: 300 16px/1.6 'open sans', sans-serif; + color: #333; +} +#tipue_search_warning +{ + font: 12px/1.6 'open sans', sans-serif; + color: #333; + margin: 7px 0; +} +#tipue_search_warning a +{ + color: #3f72d8; + text-decoration: none; +} +#tipue_search_warning a:hover +{ + padding-bottom: 1px; + border-bottom: 1px solid #ccc; +} +#tipue_search_results_count +{ + font: 13px/1.6 'open sans', sans-serif; + color: #333; +} +.tipue_search_content_title +{ + font: 300 23px/1.6 'open sans', sans-serif; + margin-top: 31px; +} +.tipue_search_content_title a +{ + color: #3f72d8; + text-decoration: none; +} +.tipue_search_content_title a:hover +{ + padding-bottom: 1px; + border-bottom: 1px solid #ccc; +} +.tipue_search_content_text +{ + font: 12px/1.7 'open sans', sans-serif; + color: #333; + padding: 13px 0; +} +.tipue_search_content_loc +{ + font: 300 13px/1.7 'open sans', sans-serif; + overflow: auto; +} +.tipue_search_content_loc a +{ + color: #555; + text-decoration: none; +} +.tipue_search_content_loc a:hover +{ + padding-bottom: 1px; + border-bottom: 1px solid #ccc; +} +#tipue_search_foot +{ + margin: 51px 0 21px 0; +} +#tipue_search_foot_boxes +{ + padding: 0; + margin: 0; + font: 12px/1 'open sans', sans-serif; +} +#tipue_search_foot_boxes li +{ + list-style: none; + margin: 0; + padding: 0; + display: inline; +} +#tipue_search_foot_boxes li a +{ + padding: 7px 13px 8px 13px; + background-color: #f1f1f1; + border: 1px solid #dcdcdc; + border-radius: 1px; + color: #333; + margin-right: 7px; + text-decoration: none; + text-align: center; +} +#tipue_search_foot_boxes li.current +{ + padding: 7px 13px 8px 13px; + background: #fff; + border: 1px solid #dcdcdc; + border-radius: 1px; + color: #333; + margin-right: 7px; + text-align: center; +} +#tipue_search_foot_boxes li a:hover +{ + border: 1px solid #ccc; + background-color: #f3f3f3; +} diff --git a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js new file mode 100644 index 0000000..a9982cd --- /dev/null +++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js @@ -0,0 +1,384 @@ + +/* +Tipue Search 3.0.1 +Copyright (c) 2013 Tipue +Tipue Search is released under the MIT License +http://www.tipue.com/search +*/ + + +(function($) { + + $.fn.tipuesearch = function(options) { + + var set = $.extend( { + + 'show' : 7, + 'newWindow' : false, + 'showURL' : true, + 'minimumLength' : 3, + 'descriptiveWords' : 25, + 'highlightTerms' : true, + 'highlightEveryTerm' : false, + 'mode' : 'static', + 'liveDescription' : '*', + 'liveContent' : '*', + 'contentLocation' : 'tipuesearch/tipuesearch_content.json' + + }, options); + + return this.each(function() { + + var tipuesearch_in = { + pages: [] + }; + $.ajaxSetup({ + async: false + }); + + if (set.mode == 'live') + { + for (var i = 0; i < tipuesearch_pages.length; i++) + { + $.get(tipuesearch_pages[i], '', + function (html) + { + var cont = $(set.liveContent, html).text(); + cont = cont.replace(/\s+/g, ' '); + var desc = $(set.liveDescription, html).text(); + desc = desc.replace(/\s+/g, ' '); + + var t_1 = html.toLowerCase().indexOf('<title>'); + var t_2 = html.toLowerCase().indexOf('</title>', t_1 + 7); + if (t_1 != -1 && t_2 != -1) + { + var tit = html.slice(t_1 + 7, t_2); + } + else + { + var tit = 'No title'; + } + + tipuesearch_in.pages.push({ + "title": tit, + "text": desc, + "tags": cont, + "loc": tipuesearch_pages[i] + }); + } + ); + } + } + + if (set.mode == 'json') + { + $.getJSON(set.contentLocation, + function(json) + { + tipuesearch_in = $.extend({}, json); + } + ); + } + + if (set.mode == 'static') + { + tipuesearch_in = $.extend({}, tipuesearch); + } + + var tipue_search_w = ''; + if (set.newWindow) + { + tipue_search_w = ' target="_blank"'; + } + + function getURLP(name) + { + return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null; + } + if (getURLP('q')) + { + $('#tipue_search_input').val(getURLP('q')); + getTipueSearch(0, true); + } + + $('#tipue_search_button').click(function() + { + getTipueSearch(0, true); + }); + $(this).keyup(function(event) + { + if(event.keyCode == '13') + { + getTipueSearch(0, true); + } + }); + + function getTipueSearch(start, replace) + { + $('#tipue_search_content').hide(); + var out = ''; + var results = ''; + var show_replace = false; + var show_stop = false; + + var d = $('#tipue_search_input').val().toLowerCase(); + d = $.trim(d); + var d_w = d.split(' '); + d = ''; + for (var i = 0; i < d_w.length; i++) + { + var a_w = true; + for (var f = 0; f < tipuesearch_stop_words.length; f++) + { + if (d_w[i] == tipuesearch_stop_words[f]) + { + a_w = false; + show_stop = true; + } + } + if (a_w) + { + d = d + ' ' + d_w[i]; + } + } + d = $.trim(d); + d_w = d.split(' '); + + if (d.length >= set.minimumLength) + { + if (replace) + { + var d_r = d; + for (var i = 0; i < d_w.length; i++) + { + for (var f = 0; f < tipuesearch_replace.words.length; f++) + { + if (d_w[i] == tipuesearch_replace.words[f].word) + { + d = d.replace(d_w[i], tipuesearch_replace.words[f].replace_with); + show_replace = true; + } + } + } + d_w = d.split(' '); + } + + var d_t = d; + for (var i = 0; i < d_w.length; i++) + { + for (var f = 0; f < tipuesearch_stem.words.length; f++) + { + if (d_w[i] == tipuesearch_stem.words[f].word) + { + d_t = d_t + ' ' + tipuesearch_stem.words[f].stem; + } + } + } + d_w = d_t.split(' '); + + var c = 0; + found = new Array(); + for (var i = 0; i < tipuesearch_in.pages.length; i++) + { + var score = 1000000000; + var s_t = tipuesearch_in.pages[i].text; + for (var f = 0; f < d_w.length; f++) + { + var pat = new RegExp(d_w[f], 'i'); + if (tipuesearch_in.pages[i].title.search(pat) != -1) + { + score -= (200000 - i); + } + if (tipuesearch_in.pages[i].text.search(pat) != -1) + { + score -= (150000 - i); + } + + if (set.highlightTerms) + { + if (set.highlightEveryTerm) + { + var patr = new RegExp('(' + d_w[f] + ')', 'gi'); + } + else + { + var patr = new RegExp('(' + d_w[f] + ')', 'i'); + } + s_t = s_t.replace(patr, "<b>$1</b>"); + } + if (tipuesearch_in.pages[i].tags.search(pat) != -1) + { + score -= (100000 - i); + } + + } + if (score < 1000000000) + { + found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc; + } + } + + if (c != 0) + { + if (show_replace == 1) + { + out += '<div id="tipue_search_warning_head">Showing results for ' + d + '</div>'; + out += '<div id="tipue_search_warning">Search for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>'; + } + if (c == 1) + { + out += '<div id="tipue_search_results_count">1 result</div>'; + } + else + { + c_c = c.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + out += '<div id="tipue_search_results_count">' + c_c + ' results</div>'; + } + + found.sort(); + var l_o = 0; + for (var i = 0; i < found.length; i++) + { + var fo = found[i].split('^'); + if (l_o >= start && l_o < set.show + start) + { + out += '<div class="tipue_search_content_title"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[1] + '</a></div>'; + + var t = fo[2]; + var t_d = ''; + var t_w = t.split(' '); + if (t_w.length < set.descriptiveWords) + { + t_d = t; + } + else + { + for (var f = 0; f < set.descriptiveWords; f++) + { + t_d += t_w[f] + ' '; + } + } + t_d = $.trim(t_d); + if (t_d.charAt(t_d.length - 1) != '.') + { + t_d += ' ...'; + } + out += '<div class="tipue_search_content_text">' + t_d + '</div>'; + + if (set.showURL) + { + t_url = fo[3]; + if (t_url.length > 45) + { + t_url = fo[3].substr(0, 45) + ' ...'; + } + out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + t_url + '</a></div>'; + } + } + l_o++; + } + + if (c > set.show) + { + var pages = Math.ceil(c / set.show); + var page = (start / set.show); + out += '<div id="tipue_search_foot"><ul id="tipue_search_foot_boxes">'; + + if (start > 0) + { + out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">Prev</a></li>'; + } + + if (page <= 2) + { + var p_b = pages; + if (pages > 3) + { + p_b = 3; + } + for (var f = 0; f < p_b; f++) + { + if (f == page) + { + out += '<li class="current">' + (f + 1) + '</li>'; + } + else + { + out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>'; + } + } + } + else + { + var p_b = page + 3; + if (p_b > pages) + { + p_b = pages; + } + for (var f = page; f < p_b; f++) + { + if (f == page) + { + out += '<li class="current">' + (f + 1) + '</li>'; + } + else + { + out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>'; + } + } + } + + if (page + 1 != pages) + { + out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next</a></li>'; + } + + out += '</ul></div>'; + } + } + else + { + out += '<div id="tipue_search_warning_head">Nothing found</div>'; + } + } + else + { + if (show_stop) + { + out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>'; + } + else + { + out += '<div id="tipue_search_warning_head">Search too short</div>'; + if (set.minimumLength == 1) + { + out += '<div id="tipue_search_warning">Should be one character or more</div>'; + } + else + { + out += '<div id="tipue_search_warning">Should be ' + set.minimumLength + ' characters or more</div>'; + } + } + } + + $('#tipue_search_content').html(out); + $('#tipue_search_content').slideDown(200); + + $('#tipue_search_replaced').click(function() + { + getTipueSearch(0, false); + }); + + $('.tipue_search_foot_box').click(function() + { + var id_v = $(this).attr('id'); + var id_a = id_v.split('_'); + + getTipueSearch(parseInt(id_a[0]), id_a[1]); + }); + } + + }); + }; + +})(jQuery); diff --git a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js new file mode 100644 index 0000000..8493ec1 --- /dev/null +++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js @@ -0,0 +1,21 @@ + +/* +Tipue Search 3.0.1 +Copyright (c) 2013 Tipue +Tipue Search is released under the MIT License +http://www.tipue.com/search +*/ + + +var tipuesearch_stop_words = ["and", "be", "by", "do", "for", "he", "how", "if", "is", "it", "my", "not", "of", "or", "the", "to", "up", "what", "when"]; + +var tipuesearch_replace = {"words": [ + {"word": "tipua", replace_with: "tipue"}, + {"word": "javscript", replace_with: "javascript"} +]}; + +var tipuesearch_stem = {"words": [ + {"word": "e-mail", stem: "email"}, + {"word": "javascript", stem: "script"}, + {"word": "javascript", stem: "js"} +]}; diff --git a/nikola/plugins/task/localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html new file mode 100755 index 0000000..789fbe5 --- /dev/null +++ b/nikola/plugins/task/localsearch/files/tipue_search.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+<head>
+<title>Tipue Search</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+
+<link rel="stylesheet" type="text/css" href="assets/css/tipuesearch.css">
+<script type="text/javascript" src="assets/js/tipuesearch_set.js"></script>
+<script type="text/javascript" src="assets/js/tipuesearch.js"></script>
+
+</head>
+<body>
+
+<div style="float: left;"><input type="text" id="tipue_search_input"></div>
+<div style="float: left; margin-left: 13px;"><input type="button" id="tipue_search_button"></div>
+<div id="tipue_search_content"><div id="tipue_search_loading"></div></div>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#tipue_search_input').tipuesearch({
+ 'mode': 'json',
+ 'contentLocation': 'assets/js/tipuesearch_content.json'
+ });
+});
+</script>
+</body>
+</html>
diff --git a/nikola/plugins/task/mustache.plugin b/nikola/plugins/task/mustache.plugin new file mode 100644 index 0000000..d6b487a --- /dev/null +++ b/nikola/plugins/task/mustache.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_mustache +Module = mustache + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Generates the blog's index pages in json. + diff --git a/nikola/plugins/task/mustache/__init__.py b/nikola/plugins/task/mustache/__init__.py new file mode 100644 index 0000000..c392e3b --- /dev/null +++ b/nikola/plugins/task/mustache/__init__.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals + +import codecs +import json +import os + +from nikola.plugin_categories import Task +from nikola.utils import config_changed, copy_file, unicode_str, makedirs + + +class Mustache(Task): + """Render the blog posts as JSON data.""" + + name = "render_mustache" + + def gen_tasks(self): + self.site.scan_posts() + + kw = { + "translations": self.site.config['TRANSLATIONS'], + "index_display_post_count": + self.site.config['INDEX_DISPLAY_POST_COUNT'], + "messages": self.site.MESSAGES, + "index_teasers": self.site.config['INDEX_TEASERS'], + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "blog_title": self.site.config['BLOG_TITLE'], + "content_footer": self.site.config['CONTENT_FOOTER'], + } + + # TODO: timeline is global, get rid of it + posts = [x for x in self.site.timeline if x.use_in_feeds] + if not posts: + yield { + 'basename': 'render_mustache', + 'actions': [], + } + return + + def write_file(path, post, lang): + + # Prev/Next links + prev_link = False + if post.prev_post: + prev_link = post.prev_post.permalink(lang).replace(".html", + ".json") + next_link = False + if post.next_post: + next_link = post.next_post.permalink(lang).replace(".html", + ".json") + data = {} + + # Configuration + for k, v in self.site.config.items(): + if isinstance(v, (str, unicode_str)): # NOQA + data[k] = v + + # Tag data + tags = [] + for tag in post.tags: + tags.append({'name': tag, 'link': self.site.link("tag", tag, + lang)}) + data.update({ + "tags": tags, + "tags?": True if tags else False, + }) + + # Template strings + for k, v in kw["messages"][lang].items(): + data["message_" + k] = v + + # Post data + data.update({ + "title": post.title(lang), + "text": post.text(lang), + "prev": prev_link, + "next": next_link, + "date": + post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']), + }) + + # Comments + context = dict(post=post, lang=self.site.current_lang()) + context.update(self.site.GLOBAL_CONTEXT) + data["comment_html"] = self.site.template_system.render_template( + 'mustache-comment-form.tmpl', None, context).strip() + + # Post translations + translations = [] + for langname in kw["translations"]: + if langname == lang: + continue + translations.append({'name': + kw["messages"][langname]["Read in English"], + 'link': "javascript:load_data('%s');" + % post.permalink(langname).replace( + ".html", ".json")}) + data["translations"] = translations + + makedirs(os.path.dirname(path)) + with codecs.open(path, 'wb+', 'utf8') as fd: + fd.write(json.dumps(data)) + + for lang in kw["translations"]: + for i, post in enumerate(posts): + out_path = post.destination_path(lang, ".json") + out_file = os.path.join(kw['output_folder'], out_path) + task = { + 'basename': 'render_mustache', + 'name': out_file, + 'file_dep': post.fragment_deps(lang), + 'targets': [out_file], + 'actions': [(write_file, (out_file, post, lang))], + 'task_dep': ['render_posts'], + 'uptodate': [config_changed({ + 1: post.text(lang), + 2: post.prev_post, + 3: post.next_post, + 4: post.title(lang), + })] + } + yield task + + if posts: + first_post_data = posts[0].permalink( + self.site.config["DEFAULT_LANG"]).replace(".html", ".json") + + # Copy mustache template + src = os.path.join(os.path.dirname(__file__), 'mustache-template.html') + dst = os.path.join(kw['output_folder'], 'mustache-template.html') + yield { + 'basename': 'render_mustache', + 'name': dst, + 'targets': [dst], + 'file_dep': [src], + 'actions': [(copy_file, (src, dst))], + } + + # Copy mustache.html with the right starting file in it + src = os.path.join(os.path.dirname(__file__), 'mustache.html') + dst = os.path.join(kw['output_folder'], 'mustache.html') + + def copy_mustache(): + with codecs.open(src, 'rb', 'utf8') as in_file: + with codecs.open(dst, 'wb+', 'utf8') as out_file: + data = in_file.read().replace('{{first_post_data}}', + first_post_data) + out_file.write(data) + yield { + 'basename': 'render_mustache', + 'name': dst, + 'targets': [dst], + 'file_dep': [src], + 'uptodate': [config_changed({1: first_post_data})], + 'actions': [(copy_mustache, [])], + } diff --git a/nikola/plugins/task/mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html new file mode 100644 index 0000000..e9a0213 --- /dev/null +++ b/nikola/plugins/task/mustache/mustache-template.html @@ -0,0 +1,29 @@ +<script id="view" type="text/html"> +<div class="container" id="container"> + <div class="postbox"> + <h1>{{BLOG_TITLE}}</h1> + <hr> + <h2>{{title}}</h2> + Posted on: {{date}}</br> + {{#tags?}} More posts about: + {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}} + </br> + {{/tags?}} + {{#translations}}<a href={{link}}>{{name}}</a>{{/translations}} </br> + <hr> + {{{text}}} + <ul class="pager"> + {{#prev}} + <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li> + {{/prev}} + {{#next}} + <li class="next"><a href="javascript:load_data('{{next}}')">{{message_Next post}}</a></li> + {{/next}} + </ul> + {{{comment_html}}} + </div> + <div class="footerbox"> + {{{CONTENT_FOOTER}}} + </div> +</div> +</script> diff --git a/nikola/plugins/task/mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html new file mode 100644 index 0000000..7ff6312 --- /dev/null +++ b/nikola/plugins/task/mustache/mustache.html @@ -0,0 +1,34 @@ +<head> + <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script> + <script src="//cdn.jsdelivr.net/jquery.mustache/0.2.7/jquery.mustache.js"></script> + <script src="//cdn.jsdelivr.net/mustache.js/0.7.2/mustache.js"></script> + <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> + <script type="text/javascript"> +function load_data(dataurl) { + jQuery.getJSON(dataurl, function(data) { + $('body').mustache('view', data, { method: 'html' }); + window.location.hash = '#' + dataurl; + }) +}; +$(document).ready(function() { +$.Mustache.load('/mustache-template.html') + .done(function () { + if (window.location.hash != '') { + load_data(window.location.hash.slice(1)); + } + else { + load_data('{{first_post_data}}'); + }; + }) +}); +</script> +</head> +<body style="padding-top: 0;"> +</body> diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin new file mode 100644 index 0000000..67212d2 --- /dev/null +++ b/nikola/plugins/task/pages.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_pages +Module = pages + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create pages in the output. + diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py new file mode 100644 index 0000000..eb5b49e --- /dev/null +++ b/nikola/plugins/task/pages.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +from nikola.plugin_categories import Task +from nikola.utils import config_changed + + +class RenderPages(Task): + """Render pages into output.""" + + name = "render_pages" + + def gen_tasks(self): + """Build final pages from metadata and HTML fragments.""" + kw = { + "post_pages": self.site.config["post_pages"], + "translations": self.site.config["TRANSLATIONS"], + "filters": self.site.config["FILTERS"], + "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + } + self.site.scan_posts() + yield self.group_task() + for lang in kw["translations"]: + for post in self.site.timeline: + if kw["hide_untranslated_posts"] and not post.is_translation_available(lang): + continue + for task in self.site.generic_page_renderer(lang, post, + kw["filters"]): + task['uptodate'] = [config_changed({ + 1: task['uptodate'][0].config, + 2: kw})] + task['basename'] = self.name + task['task_dep'] = ['render_posts'] + yield task diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin new file mode 100644 index 0000000..e1a42fd --- /dev/null +++ b/nikola/plugins/task/posts.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_posts +Module = posts + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create HTML fragments out of posts. + diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py new file mode 100644 index 0000000..18d61b8 --- /dev/null +++ b/nikola/plugins/task/posts.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from copy import copy +import nikola.post + +from nikola.plugin_categories import Task +from nikola import utils + + +class RenderPosts(Task): + """Build HTML fragments from metadata and text.""" + + name = "render_posts" + + def gen_tasks(self): + """Build HTML fragments from metadata and text.""" + self.site.scan_posts() + kw = { + "translations": self.site.config["TRANSLATIONS"], + "timeline": self.site.timeline, + "default_lang": self.site.config["DEFAULT_LANG"], + "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + } + + nikola.post.READ_MORE_LINK = self.site.config['READ_MORE_LINK'] + yield self.group_task() + + for lang in kw["translations"]: + deps_dict = copy(kw) + deps_dict.pop('timeline') + for post in kw['timeline']: + dest = post.translated_base_path(lang) + task = { + 'basename': self.name, + 'name': dest, + 'file_dep': post.fragment_deps(lang), + 'targets': [dest], + 'actions': [(post.compile, (lang, ))], + 'clean': True, + 'uptodate': [utils.config_changed(deps_dict)], + } + yield task diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin new file mode 100644 index 0000000..826f3d8 --- /dev/null +++ b/nikola/plugins/task/redirect.plugin @@ -0,0 +1,10 @@ +[Core] +Name = redirect +Module = redirect + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create redirect pages. + diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py new file mode 100644 index 0000000..ade878a --- /dev/null +++ b/nikola/plugins/task/redirect.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import codecs +import os + +from nikola.plugin_categories import Task +from nikola import utils + + +class Redirect(Task): + """Generate redirections""" + + name = "redirect" + + def gen_tasks(self): + """Generate redirections tasks.""" + + kw = { + 'redirections': self.site.config['REDIRECTIONS'], + 'output_folder': self.site.config['OUTPUT_FOLDER'], + } + + yield self.group_task() + if kw['redirections']: + for src, dst in kw["redirections"]: + src_path = os.path.join(kw["output_folder"], src) + yield { + 'basename': self.name, + 'name': src_path, + 'targets': [src_path], + 'actions': [(create_redirect, (src_path, dst))], + 'clean': True, + 'uptodate': [utils.config_changed(kw)], + } + + +def create_redirect(src, dst): + utils.makedirs(os.path.dirname(src)) + with codecs.open(src, "wb+", "utf8") as fd: + fd.write('<!DOCTYPE html><head><title>Redirecting...</title>' + '<meta http-equiv="refresh" content="0; ' + 'url={0}"></head>'.format(dst)) diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin new file mode 100644 index 0000000..7206a43 --- /dev/null +++ b/nikola/plugins/task/rss.plugin @@ -0,0 +1,10 @@ +[Core] +Name = generate_rss +Module = rss + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Generate RSS feeds. + diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py new file mode 100644 index 0000000..bcca4da --- /dev/null +++ b/nikola/plugins/task/rss.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals, print_function +import os +try: + from urlparse import urljoin +except ImportError: + from urllib.parse import urljoin # NOQA + +from nikola import utils +from nikola.plugin_categories import Task + + +class GenerateRSS(Task): + """Generate RSS feeds.""" + + name = "generate_rss" + + def set_site(self, site): + site.register_path_handler('rss', self.rss_path) + return super(GenerateRSS, self).set_site(site) + + def gen_tasks(self): + """Generate RSS feeds.""" + kw = { + "translations": self.site.config["TRANSLATIONS"], + "filters": self.site.config["FILTERS"], + "blog_title": self.site.config["BLOG_TITLE"], + "site_url": self.site.config["SITE_URL"], + "blog_description": self.site.config["BLOG_DESCRIPTION"], + "output_folder": self.site.config["OUTPUT_FOLDER"], + "rss_teasers": self.site.config["RSS_TEASERS"], + "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "feed_length": self.site.config['FEED_LENGTH'], + } + self.site.scan_posts() + yield self.group_task() + for lang in kw["translations"]: + output_name = os.path.join(kw['output_folder'], + self.site.path("rss", None, lang)) + deps = [] + if kw["hide_untranslated_posts"]: + posts = [x for x in self.site.timeline if x.use_in_feeds + and x.is_translation_available(lang)][:10] + else: + posts = [x for x in self.site.timeline if x.use_in_feeds][:10] + for post in posts: + deps += post.deps(lang) + + feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/')) + yield { + 'basename': 'generate_rss', + 'name': os.path.normpath(output_name), + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, kw["blog_title"], kw["site_url"], + kw["blog_description"], posts, output_name, + kw["rss_teasers"], kw['feed_length'], feed_url))], + 'task_dep': ['render_posts'], + 'clean': True, + 'uptodate': [utils.config_changed(kw)], + } + + def rss_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['RSS_PATH'], 'rss.xml'] if _f] diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin new file mode 100644 index 0000000..2cd8195 --- /dev/null +++ b/nikola/plugins/task/sitemap.plugin @@ -0,0 +1,10 @@ +[Core] +Name = sitemap +Module = sitemap + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Generate google sitemap. + diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py new file mode 100644 index 0000000..f34bc0a --- /dev/null +++ b/nikola/plugins/task/sitemap/__init__.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import print_function, absolute_import, unicode_literals +import codecs +import datetime +import os +try: + from urlparse import urljoin, urlparse +except ImportError: + from urllib.parse import urljoin, urlparse # NOQA + +from nikola.plugin_categories import LateTask +from nikola.utils import config_changed + + +header = """<?xml version="1.0" encoding="UTF-8"?> +<urlset + xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 + http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> +""" + +url_format = """ <url> + <loc>{0}</loc> + <lastmod>{1}</lastmod> + </url> +""" + +get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0] + + +def get_base_path(base): + """returns the path of a base URL if it contains one. + + >>> get_base_path('http://some.site') == '/' + True + >>> get_base_path('http://some.site/') == '/' + True + >>> get_base_path('http://some.site/some/sub-path') == '/some/sub-path/' + True + >>> get_base_path('http://some.site/some/sub-path/') == '/some/sub-path/' + True + """ + # first parse the base_url for some path + base_parsed = urlparse(base) + + if not base_parsed.path: + sub_path = '' + else: + sub_path = base_parsed.path + if sub_path.endswith('/'): + return sub_path + else: + return sub_path + '/' + + +class Sitemap(LateTask): + """Generate google sitemap.""" + + name = "sitemap" + + def gen_tasks(self): + """Generate Google sitemap.""" + kw = { + "base_url": self.site.config["BASE_URL"], + "site_url": self.site.config["SITE_URL"], + "output_folder": self.site.config["OUTPUT_FOLDER"], + "strip_indexes": self.site.config["STRIP_INDEXES"], + "index_file": self.site.config["INDEX_FILE"], + "sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"], + "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm', '.xml']) + } + output_path = kw['output_folder'] + sitemap_path = os.path.join(output_path, "sitemap.xml") + base_path = get_base_path(kw['base_url']) + locs = {} + + output = kw['output_folder'] + base_url = kw['base_url'] + mapped_exts = kw['mapped_extensions'] + + def scan_locs(): + for root, dirs, files in os.walk(output): + if not dirs and not files and not kw['sitemap_include_fileless_dirs']: + continue # Totally empty, not on sitemap + path = os.path.relpath(root, output) + # ignore the current directory. + path = (path.replace(os.sep, '/') + '/').replace('./', '') + lastmod = get_lastmod(root) + loc = urljoin(base_url, base_path + path) + if kw['index_file'] in files and kw['strip_indexes']: # ignore folders when not stripping urls + locs[loc] = url_format.format(loc, lastmod) + for fname in files: + if kw['strip_indexes'] and fname == kw['index_file']: + continue # We already mapped the folder + if os.path.splitext(fname)[-1] in mapped_exts: + real_path = os.path.join(root, fname) + path = os.path.relpath(real_path, output) + if path.endswith(kw['index_file']) and kw['strip_indexes']: + # ignore index files when stripping urls + continue + if path.endswith('.html') or path.endswith('.htm'): + if not u'<!doctype html' in codecs.open(real_path, 'r', 'utf8').read(1024).lower(): + # ignores "html" files without doctype + # alexa-verify, google-site-verification, etc. + continue + if path.endswith('.xml'): + if not u'<rss' in codecs.open(real_path, 'r', 'utf8').read(512): + # ignores all XML files except those presumed to be RSS + continue + post = self.site.post_per_file.get(path) + if post and (post.is_draft or post.is_retired or post.publish_later): + continue + path = path.replace(os.sep, '/') + lastmod = get_lastmod(real_path) + loc = urljoin(base_url, base_path + path) + locs[loc] = url_format.format(loc, lastmod) + + def write_sitemap(): + # Have to rescan, because files may have been added between + # task dep scanning and task execution + scan_locs() + with codecs.open(sitemap_path, 'wb+', 'utf8') as outf: + outf.write(header) + for k in sorted(locs.keys()): + outf.write(locs[k]) + outf.write("</urlset>") + # Other tasks can depend on this output, instead of having + # to scan locations. + return {'locations': list(locs.keys())} + + scan_locs() + yield self.group_task() + task = { + "basename": "sitemap", + "name": sitemap_path, + "targets": [sitemap_path], + "actions": [(write_sitemap,)], + "uptodate": [config_changed({1: kw, 2: locs})], + "clean": True, + "task_dep": ["render_site"], + } + yield task + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin new file mode 100644 index 0000000..6224e48 --- /dev/null +++ b/nikola/plugins/task/sources.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_sources +Module = sources + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Copy page sources into the output. + diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py new file mode 100644 index 0000000..672f354 --- /dev/null +++ b/nikola/plugins/task/sources.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import os + +from nikola.plugin_categories import Task +from nikola import utils + + +class Sources(Task): + """Copy page sources into the output.""" + + name = "render_sources" + + def gen_tasks(self): + """Publish the page sources into the output. + + Required keyword arguments: + + translations + default_lang + post_pages + output_folder + """ + kw = { + "translations": self.site.config["TRANSLATIONS"], + "output_folder": self.site.config["OUTPUT_FOLDER"], + "default_lang": self.site.config["DEFAULT_LANG"], + } + + self.site.scan_posts() + yield self.group_task() + if self.site.config['COPY_SOURCES']: + for lang in kw["translations"]: + for post in self.site.timeline: + if post.meta('password'): + continue + output_name = os.path.join( + kw['output_folder'], post.destination_path( + lang, post.source_ext())) + source = post.source_path + dest_ext = self.site.get_compiler(post.source_path).extension() + if dest_ext == post.source_ext(): + continue + if lang != kw["default_lang"]: + source_lang = source + '.' + lang + if os.path.exists(source_lang): + source = source_lang + if os.path.isfile(source): + yield { + 'basename': 'render_sources', + 'name': os.path.normpath(output_name), + 'file_dep': [source], + 'targets': [output_name], + 'actions': [(utils.copy_file, (source, output_name))], + 'clean': True, + 'uptodate': [utils.config_changed(kw)], + } diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin new file mode 100644 index 0000000..f01e0f8 --- /dev/null +++ b/nikola/plugins/task/tags.plugin @@ -0,0 +1,10 @@ +[Core] +Name = render_tags +Module = tags + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Render the tag pages and feeds. + diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py new file mode 100644 index 0000000..299dca4 --- /dev/null +++ b/nikola/plugins/task/tags.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +import codecs +import json +import os +try: + from urlparse import urljoin +except ImportError: + from urllib.parse import urljoin # NOQA + +from nikola.plugin_categories import Task +from nikola import utils + + +class RenderTags(Task): + """Render the tag/category pages and feeds.""" + + name = "render_tags" + + def set_site(self, site): + site.register_path_handler('tag_index', self.tag_index_path) + site.register_path_handler('tag', self.tag_path) + site.register_path_handler('tag_rss', self.tag_rss_path) + site.register_path_handler('category', self.category_path) + site.register_path_handler('category_rss', self.category_rss_path) + return super(RenderTags, self).set_site(site) + + def gen_tasks(self): + """Render the tag pages and feeds.""" + + kw = { + "translations": self.site.config["TRANSLATIONS"], + "blog_title": self.site.config["BLOG_TITLE"], + "site_url": self.site.config["SITE_URL"], + "messages": self.site.MESSAGES, + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'], + "index_display_post_count": + self.site.config['INDEX_DISPLAY_POST_COUNT'], + "index_teasers": self.site.config['INDEX_TEASERS'], + "rss_teasers": self.site.config["RSS_TEASERS"], + "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "feed_length": self.site.config['FEED_LENGTH'], + } + + self.site.scan_posts() + yield self.group_task() + + yield self.list_tags_page(kw) + + if not self.site.posts_per_tag and not self.site.posts_per_category: + return + + tag_list = list(self.site.posts_per_tag.items()) + cat_list = list(self.site.posts_per_category.items()) + + def render_lists(tag, posts, is_category=True): + post_list = [self.site.global_data[post] for post in posts] + post_list.sort(key=lambda a: a.date) + post_list.reverse() + for lang in kw["translations"]: + if kw["hide_untranslated_posts"]: + filtered_posts = [x for x in post_list if x.is_translation_available(lang)] + else: + filtered_posts = post_list + rss_post_list = [p.source_path for p in filtered_posts] + yield self.tag_rss(tag, lang, rss_post_list, kw, is_category) + # Render HTML + if kw['tag_pages_are_indexes']: + yield self.tag_page_as_index(tag, lang, filtered_posts, kw, is_category) + else: + yield self.tag_page_as_list(tag, lang, filtered_posts, kw, is_category) + + for tag, posts in tag_list: + for task in render_lists(tag, posts, False): + yield task + + for tag, posts in cat_list: + if tag == '': # This is uncategorized posts + continue + for task in render_lists(tag, posts, True): + yield task + + # Tag cloud json file + tag_cloud_data = {} + for tag, posts in self.site.posts_per_tag.items(): + tag_cloud_data[tag] = [len(posts), self.site.link( + 'tag', tag, self.site.config['DEFAULT_LANG'])] + output_name = os.path.join(kw['output_folder'], + 'assets', 'js', 'tag_cloud_data.json') + + def write_tag_data(data): + utils.makedirs(os.path.dirname(output_name)) + with codecs.open(output_name, 'wb+', 'utf8') as fd: + fd.write(json.dumps(data)) + + task = { + 'basename': str(self.name), + 'name': str(output_name) + } + task['uptodate'] = [utils.config_changed(tag_cloud_data)] + task['targets'] = [output_name] + task['actions'] = [(write_tag_data, [tag_cloud_data])] + task['clean'] = True + yield task + + def list_tags_page(self, kw): + """a global "all your tags/categories" page for each language""" + tags = list(self.site.posts_per_tag.keys()) + categories = list(self.site.posts_per_category.keys()) + # We want our tags to be sorted case insensitive + tags.sort(key=lambda a: a.lower()) + categories.sort(key=lambda a: a.lower()) + if categories != ['']: + has_categories = True + else: + has_categories = False + template_name = "tags.tmpl" + kw['tags'] = tags + kw['categories'] = categories + for lang in kw["translations"]: + output_name = os.path.join( + kw['output_folder'], self.site.path('tag_index', None, lang)) + output_name = output_name + context = {} + if has_categories: + context["title"] = kw["messages"][lang]["Tags and Categories"] + else: + context["title"] = kw["messages"][lang]["Tags"] + context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag + in tags] + if has_categories: + context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag + in categories] + else: + context["cat_items"] = None + context["permalink"] = self.site.link("tag_index", None, lang) + context["description"] = None + task = self.site.generic_post_list_renderer( + lang, + [], + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [utils.config_changed(task_cfg)] + task['basename'] = str(self.name) + yield task + + def tag_page_as_index(self, tag, lang, post_list, kw, is_category): + """render a sort of index page collection using only this + tag's posts.""" + + kind = "category" if is_category else "tag" + + def page_name(tagname, i, lang): + """Given tag, n, returns a page name.""" + name = self.site.path(kind, tag, lang) + if i: + name = name.replace('.html', '-{0}.html'.format(i)) + return name + + # 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 include the tag's feeds + rss_link = ("""<link rel="alternate" type="application/rss+xml" """ + """type="application/rss+xml" title="RSS for tag """ + """{0} ({1})" href="{2}">""".format( + tag, lang, self.site.link(kind + "_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][ + "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.site.link(kind, tag, lang) + context["tag"] = tag + context["description"] = None + task = self.site.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [utils.config_changed(task_cfg)] + task['basename'] = str(self.name) + + yield task + + def tag_page_as_list(self, tag, lang, post_list, kw, is_category): + """We render a single flat link list with this tag's posts""" + kind = "category" if is_category else "tag" + template_name = "tag.tmpl" + output_name = os.path.join(kw['output_folder'], self.site.path( + kind, tag, lang)) + context = {} + context["lang"] = lang + context["title"] = kw["messages"][lang]["Posts about %s"] % tag + context["posts"] = post_list + context["permalink"] = self.site.link(kind, tag, lang) + context["tag"] = tag + context["kind"] = kind + context["description"] = None + task = self.site.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [utils.config_changed(task_cfg)] + task['basename'] = str(self.name) + yield task + + def tag_rss(self, tag, lang, posts, kw, is_category): + """RSS for a single tag / language""" + kind = "category" if is_category else "tag" + #Render RSS + output_name = os.path.normpath( + os.path.join(kw['output_folder'], + self.site.path(kind + "_rss", tag, lang))) + feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/')) + deps = [] + post_list = [self.site.global_data[post] for post in posts if + self.site.global_data[post].use_in_feeds] + post_list.sort(key=lambda a: a.date) + post_list.reverse() + for post in post_list: + deps += post.deps(lang) + return { + 'basename': str(self.name), + 'name': output_name, + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, "{0} ({1})".format(kw["blog_title"], tag), + kw["site_url"], None, post_list, + output_name, kw["rss_teasers"], kw['feed_length'], feed_url))], + 'clean': True, + 'uptodate': [utils.config_changed(kw)], + 'task_dep': ['render_posts'], + } + + def slugify_name(self, name): + if self.site.config['SLUG_TAG_PATH']: + name = utils.slugify(name) + return name + + def tag_index_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], + self.site.config['INDEX_FILE']] if _f] + + def tag_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], self.slugify_name(name) + ".html"] if + _f] + + def tag_rss_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], self.slugify_name(name) + ".xml"] if + _f] + + def category_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".html"] if + _f] + + def category_rss_path(self, name, lang): + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".xml"] if + _f] |
