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 | |
| parent | f794eee787e9cde54e6b8f53e45d69c9ddc9936a (diff) | |
Imported Upstream version 6.2.1upstream/6.2.1
Diffstat (limited to 'nikola/plugins')
| -rw-r--r-- | nikola/plugins/__init__.py | 3 | ||||
| -rw-r--r-- | nikola/plugins/basic_import.py | 166 | ||||
| -rw-r--r-- | nikola/plugins/command/__init__.py | 25 | ||||
| -rw-r--r-- | nikola/plugins/command/auto.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/command/auto.py | 103 | ||||
| -rw-r--r-- | nikola/plugins/command/bootswatch_theme.plugin (renamed from nikola/plugins/command_bootswatch_theme.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/bootswatch_theme.py (renamed from nikola/plugins/command_bootswatch_theme.py) | 49 | ||||
| -rw-r--r-- | nikola/plugins/command/check.plugin (renamed from nikola/plugins/command_check.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/check.py | 204 | ||||
| -rw-r--r-- | nikola/plugins/command/console.plugin (renamed from nikola/plugins/command_console.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/console.py (renamed from nikola/plugins/command_console.py) | 27 | ||||
| -rw-r--r-- | nikola/plugins/command/deploy.plugin (renamed from nikola/plugins/command_deploy.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/deploy.py | 141 | ||||
| -rw-r--r-- | nikola/plugins/command/import_blogger.plugin (renamed from nikola/plugins/command_import_blogger.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/import_blogger.py (renamed from nikola/plugins/command_import_blogger.py) | 159 | ||||
| -rw-r--r-- | nikola/plugins/command/import_feed.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/command/import_feed.py | 197 | ||||
| -rw-r--r-- | nikola/plugins/command/import_wordpress.plugin (renamed from nikola/plugins/command_import_wordpress.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/import_wordpress.py (renamed from nikola/plugins/command_import_wordpress.py) | 256 | ||||
| -rw-r--r-- | nikola/plugins/command/init.plugin (renamed from nikola/plugins/command_init.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/init.py (renamed from nikola/plugins/command_init.py) | 45 | ||||
| -rw-r--r-- | nikola/plugins/command/install_plugin.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/command/install_plugin.py | 185 | ||||
| -rw-r--r-- | nikola/plugins/command/install_theme.plugin (renamed from nikola/plugins/command_install_theme.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/install_theme.py | 163 | ||||
| -rw-r--r-- | nikola/plugins/command/mincss.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/command/mincss.py | 75 | ||||
| -rw-r--r-- | nikola/plugins/command/new_post.plugin (renamed from nikola/plugins/command_new_post.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/new_post.py (renamed from nikola/plugins/command_new_post.py) | 108 | ||||
| -rw-r--r-- | nikola/plugins/command/planetoid.plugin (renamed from nikola/plugins/command_planetoid.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/planetoid/__init__.py (renamed from nikola/plugins/command_planetoid/__init__.py) | 30 | ||||
| -rw-r--r-- | nikola/plugins/command/serve.plugin (renamed from nikola/plugins/command_serve.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/command/serve.py | 153 | ||||
| -rw-r--r-- | nikola/plugins/command/version.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/command/version.py (renamed from nikola/plugins/compile_rest/dummy.py) | 28 | ||||
| -rw-r--r-- | nikola/plugins/command_check.py | 166 | ||||
| -rw-r--r-- | nikola/plugins/command_deploy.py | 65 | ||||
| -rw-r--r-- | nikola/plugins/command_install_theme.py | 105 | ||||
| -rw-r--r-- | nikola/plugins/command_serve.py | 79 | ||||
| -rw-r--r-- | nikola/plugins/compile/__init__.py | 0 | ||||
| -rw-r--r-- | nikola/plugins/compile/asciidoc.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/asciidoc.py | 65 | ||||
| -rw-r--r-- | nikola/plugins/compile/bbcode.plugin (renamed from nikola/plugins/compile_bbcode.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/bbcode.py (renamed from nikola/plugins/compile_bbcode.py) | 24 | ||||
| -rw-r--r-- | nikola/plugins/compile/html.plugin (renamed from nikola/plugins/compile_html.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/html.py (renamed from nikola/plugins/compile_html.py) | 21 | ||||
| -rw-r--r-- | nikola/plugins/compile/ipynb.plugin (renamed from nikola/plugins/compile_ipynb.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/ipynb/README.txt | 44 | ||||
| -rw-r--r-- | nikola/plugins/compile/ipynb/__init__.py (renamed from nikola/plugins/compile_ipynb/__init__.py) | 46 | ||||
| -rw-r--r-- | nikola/plugins/compile/markdown.plugin (renamed from nikola/plugins/compile_markdown.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/markdown/__init__.py (renamed from nikola/plugins/compile_markdown/__init__.py) | 34 | ||||
| -rw-r--r-- | nikola/plugins/compile/markdown/mdx_gist.py (renamed from nikola/plugins/compile_markdown/mdx_gist.py) | 122 | ||||
| -rw-r--r-- | nikola/plugins/compile/markdown/mdx_nikola.py (renamed from nikola/plugins/compile_markdown/mdx_nikola.py) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/markdown/mdx_podcast.py (renamed from nikola/plugins/compile_markdown/mdx_podcast.py) | 0 | ||||
| -rw-r--r-- | nikola/plugins/compile/misaka.plugin (renamed from nikola/plugins/compile_misaka.plugin) | 2 | ||||
| -rw-r--r-- | nikola/plugins/compile/misaka.py (renamed from nikola/plugins/compile_misaka/__init__.py) | 29 | ||||
| -rw-r--r-- | nikola/plugins/compile/pandoc.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/pandoc.py | 65 | ||||
| -rw-r--r-- | nikola/plugins/compile/php.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/php.py | 62 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest.plugin (renamed from nikola/plugins/compile_rest.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/__init__.py | 200 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/chart.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/chart.py | 150 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/doc.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/doc.py | 88 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/gist.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/gist.py (renamed from nikola/plugins/compile_rest/gist_directive.py) | 48 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/listing.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/listing.py (renamed from nikola/plugins/compile_rest/listing.py) | 230 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/media.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/media.py | 63 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/post_list.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/post_list.py | 165 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/slides.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/slides.py (renamed from nikola/plugins/compile_rest/slides.py) | 54 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/soundcloud.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/soundcloud.py (renamed from nikola/plugins/compile_rest/soundcloud.py) | 18 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/vimeo.plugin | 7 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/vimeo.py (renamed from nikola/plugins/compile_rest/vimeo.py) | 43 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/youtube.plugin | 8 | ||||
| -rw-r--r-- | nikola/plugins/compile/rest/youtube.py (renamed from nikola/plugins/compile_rest/youtube.py) | 20 | ||||
| -rw-r--r-- | nikola/plugins/compile/textile.plugin (renamed from nikola/plugins/compile_textile.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/textile.py (renamed from nikola/plugins/compile_textile.py) | 22 | ||||
| -rw-r--r-- | nikola/plugins/compile/txt2tags.plugin (renamed from nikola/plugins/compile_txt2tags.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/txt2tags.py (renamed from nikola/plugins/compile_txt2tags.py) | 19 | ||||
| -rw-r--r-- | nikola/plugins/compile/wiki.plugin (renamed from nikola/plugins/compile_wiki.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/compile/wiki.py (renamed from nikola/plugins/compile_wiki.py) | 25 | ||||
| -rw-r--r-- | nikola/plugins/compile_ipynb/README.txt | 35 | ||||
| -rw-r--r-- | nikola/plugins/compile_rest/__init__.py | 138 | ||||
| -rw-r--r-- | nikola/plugins/loghandler/smtp.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/loghandler/smtp.py | 54 | ||||
| -rw-r--r-- | nikola/plugins/loghandler/stderr.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/loghandler/stderr.py | 50 | ||||
| -rw-r--r-- | nikola/plugins/task/__init__.py | 0 | ||||
| -rw-r--r-- | nikola/plugins/task/archive.plugin (renamed from nikola/plugins/task_archive.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/archive.py (renamed from nikola/plugins/task_archive.py) | 104 | ||||
| -rw-r--r-- | nikola/plugins/task/build_less.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/task/build_less.py | 99 | ||||
| -rw-r--r-- | nikola/plugins/task/build_sass.plugin | 9 | ||||
| -rw-r--r-- | nikola/plugins/task/build_sass.py | 117 | ||||
| -rw-r--r-- | nikola/plugins/task/bundles.plugin (renamed from nikola/plugins/task_create_bundles.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/bundles.py (renamed from nikola/plugins/task_create_bundles.py) | 46 | ||||
| -rw-r--r-- | nikola/plugins/task/copy_assets.plugin (renamed from nikola/plugins/task_copy_assets.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/copy_assets.py (renamed from nikola/plugins/task_copy_assets.py) | 18 | ||||
| -rw-r--r-- | nikola/plugins/task/copy_files.plugin (renamed from nikola/plugins/task_copy_files.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/copy_files.py (renamed from nikola/plugins/task_copy_files.py) | 12 | ||||
| -rw-r--r-- | nikola/plugins/task/galleries.plugin (renamed from nikola/plugins/task_render_galleries.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/galleries.py | 553 | ||||
| -rw-r--r-- | nikola/plugins/task/gzip.plugin | 10 | ||||
| -rw-r--r-- | nikola/plugins/task/gzip.py | 78 | ||||
| -rw-r--r-- | nikola/plugins/task/indexes.plugin (renamed from nikola/plugins/task_indexes.plugin) | 6 | ||||
| -rw-r--r-- | nikola/plugins/task/indexes.py (renamed from nikola/plugins/task_indexes.py) | 82 | ||||
| -rw-r--r-- | nikola/plugins/task/listings.plugin (renamed from nikola/plugins/task_render_listings.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/listings.py (renamed from nikola/plugins/task_render_listings.py) | 31 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch.plugin (renamed from nikola/plugins/task_localsearch.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch/MIT-LICENSE.txt (renamed from nikola/plugins/task_localsearch/MIT-LICENSE.txt) | 0 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch/__init__.py (renamed from nikola/plugins/task_localsearch/__init__.py) | 20 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch/files/assets/css/img/loader.gif (renamed from nikola/plugins/task_localsearch/files/assets/css/img/loader.gif) | bin | 4178 -> 4178 bytes | |||
| -rwxr-xr-x | nikola/plugins/task/localsearch/files/assets/css/img/search.png | bin | 0 -> 315 bytes | |||
| -rwxr-xr-x | nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css | 159 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js) | 174 | ||||
| -rw-r--r-- | nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js) | 11 | ||||
| -rwxr-xr-x | nikola/plugins/task/localsearch/files/tipue_search.html (renamed from nikola/plugins/task_localsearch/files/tipue_search.html) | 0 | ||||
| -rw-r--r-- | nikola/plugins/task/mustache.plugin (renamed from nikola/plugins/task_mustache.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/mustache/__init__.py (renamed from nikola/plugins/task_mustache/__init__.py) | 35 | ||||
| -rw-r--r-- | nikola/plugins/task/mustache/mustache-template.html (renamed from nikola/plugins/task_mustache/mustache-template.html) | 6 | ||||
| -rw-r--r-- | nikola/plugins/task/mustache/mustache.html (renamed from nikola/plugins/task_mustache/mustache.html) | 10 | ||||
| -rw-r--r-- | nikola/plugins/task/pages.plugin (renamed from nikola/plugins/task_render_pages.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/pages.py (renamed from nikola/plugins/task_render_pages.py) | 14 | ||||
| -rw-r--r-- | nikola/plugins/task/posts.plugin (renamed from nikola/plugins/task_render_posts.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/posts.py | 66 | ||||
| -rw-r--r-- | nikola/plugins/task/redirect.plugin (renamed from nikola/plugins/task_redirect.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/redirect.py (renamed from nikola/plugins/task_redirect.py) | 24 | ||||
| -rw-r--r-- | nikola/plugins/task/rss.plugin (renamed from nikola/plugins/task_render_rss.plugin) | 6 | ||||
| -rw-r--r-- | nikola/plugins/task/rss.py (renamed from nikola/plugins/task_render_rss.py) | 29 | ||||
| -rw-r--r-- | nikola/plugins/task/sitemap.plugin (renamed from nikola/plugins/task_sitemap.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/sitemap/__init__.py | 172 | ||||
| -rw-r--r-- | nikola/plugins/task/sources.plugin (renamed from nikola/plugins/task_render_sources.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/sources.py (renamed from nikola/plugins/task_render_sources.py) | 63 | ||||
| -rw-r--r-- | nikola/plugins/task/tags.plugin (renamed from nikola/plugins/task_render_tags.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/task/tags.py (renamed from nikola/plugins/task_render_tags.py) | 137 | ||||
| -rwxr-xr-x | nikola/plugins/task_localsearch/files/assets/css/img/expand.png | bin | 424 -> 0 bytes | |||
| -rwxr-xr-x | nikola/plugins/task_localsearch/files/assets/css/img/link.png | bin | 463 -> 0 bytes | |||
| -rw-r--r-- | nikola/plugins/task_localsearch/files/assets/css/img/search.gif | bin | 208 -> 0 bytes | |||
| -rwxr-xr-x | nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css | 232 | ||||
| -rw-r--r-- | nikola/plugins/task_render_galleries.py | 338 | ||||
| -rw-r--r-- | nikola/plugins/task_render_posts.py | 140 | ||||
| -rw-r--r-- | nikola/plugins/task_sitemap/__init__.py | 105 | ||||
| -rw-r--r-- | nikola/plugins/template/__init__.py | 0 | ||||
| -rw-r--r-- | nikola/plugins/template/jinja.plugin (renamed from nikola/plugins/template_jinja.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/template/jinja.py (renamed from nikola/plugins/template_jinja.py) | 48 | ||||
| -rw-r--r-- | nikola/plugins/template/mako.plugin (renamed from nikola/plugins/template_mako.plugin) | 4 | ||||
| -rw-r--r-- | nikola/plugins/template/mako.py (renamed from nikola/plugins/template_mako.py) | 41 |
154 files changed, 5253 insertions, 2554 deletions
diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py index b1de7f1..139759b 100644 --- a/nikola/plugins/__init__.py +++ b/nikola/plugins/__init__.py @@ -1,3 +1,2 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import - -from . import command_import_wordpress # NOQA diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py new file mode 100644 index 0000000..e368fca --- /dev/null +++ b/nikola/plugins/basic_import.py @@ -0,0 +1,166 @@ +# -*- 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 codecs +import csv +import datetime +import os + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # NOQA + +from lxml import etree, html +from mako.template import Template + +from nikola import utils + +links = {} + + +class ImportMixin(object): + """Mixin with common used methods.""" + + name = "import_mixin" + needs_config = False + doc_usage = "[options] wordpress_export_file" + doc_purpose = "import a wordpress dump." + cmd_options = [ + { + 'name': 'output_folder', + 'long': 'output-folder', + 'short': 'o', + 'default': 'new_site', + 'help': 'Location to write imported content.' + }, + ] + + def _execute(self, options={}, args=[]): + """Import a blog from an export into a Nikola site.""" + raise NotImplementedError("Must be implemented by a subclass.") + + @classmethod + def get_channel_from_file(cls, filename): + tree = etree.fromstring(cls.read_xml_file(filename)) + channel = tree.find('channel') + return channel + + @staticmethod + def configure_redirections(url_map): + redirections = [] + for k, v in url_map.items(): + if not k[-1] == '/': + k = k + '/' + + # remove the initial "/" because src is a relative file path + src = (urlparse(k).path + 'index.html')[1:] + dst = (urlparse(v).path) + if src == 'index.html': + utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k)) + else: + redirections.append((src, dst)) + + return redirections + + def generate_base_site(self): + if not os.path.exists(self.output_folder): + os.system('nikola init ' + self.output_folder) + else: + self.import_into_existing_site = True + utils.LOGGER.notice('The folder {0} already exists - assuming that this is a ' + 'already existing nikola site.'.format(self.output_folder)) + + filename = os.path.join(os.path.dirname(utils.__file__), 'conf.py.in') + # The 'strict_undefined=True' will give the missing symbol name if any, + # (ex: NameError: 'THEME' is not defined ) + # for other errors from mako/runtime.py, you can add format_extensions=True , + # then more info will be writen to *somefile* (most probably conf.py) + conf_template = Template(filename=filename, strict_undefined=True) + + return conf_template + + @staticmethod + def populate_context(channel): + raise NotImplementedError("Must be implemented by a subclass.") + + @classmethod + def transform_content(cls, content): + return content + + @classmethod + def write_content(cls, filename, content): + doc = html.document_fromstring(content) + doc.rewrite_links(replacer) + + utils.makedirs(os.path.dirname(filename)) + with open(filename, "wb+") as fd: + fd.write(html.tostring(doc, encoding='utf8')) + + @staticmethod + def write_metadata(filename, title, slug, post_date, description, tags): + if not description: + description = "" + + utils.makedirs(os.path.dirname(filename)) + with codecs.open(filename, "w+", "utf8") as fd: + fd.write('{0}\n'.format(title)) + fd.write('{0}\n'.format(slug)) + fd.write('{0}\n'.format(post_date)) + fd.write('{0}\n'.format(','.join(tags))) + fd.write('\n') + fd.write('{0}\n'.format(description)) + + @staticmethod + def write_urlmap_csv(output_file, url_map): + utils.makedirs(os.path.dirname(output_file)) + with codecs.open(output_file, 'w+', 'utf8') as fd: + csv_writer = csv.writer(fd) + for item in url_map.items(): + csv_writer.writerow(item) + + def get_configuration_output_path(self): + if not self.import_into_existing_site: + filename = 'conf.py' + else: + filename = 'conf.py.{name}-{time}'.format( + time=datetime.datetime.now().strftime('%Y%m%d_%H%M%S'), + name=self.name) + config_output_path = os.path.join(self.output_folder, filename) + utils.LOGGER.notice('Configuration will be written to: {0}'.format(config_output_path)) + + return config_output_path + + @staticmethod + def write_configuration(filename, rendered_template): + utils.makedirs(os.path.dirname(filename)) + with codecs.open(filename, 'w+', 'utf8') as fd: + fd.write(rendered_template) + + +def replacer(dst): + return links.get(dst, dst) diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py new file mode 100644 index 0000000..9be4d63 --- /dev/null +++ b/nikola/plugins/command/__init__.py @@ -0,0 +1,25 @@ +# -*- 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. diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin new file mode 100644 index 0000000..87939b2 --- /dev/null +++ b/nikola/plugins/command/auto.plugin @@ -0,0 +1,9 @@ +[Core] +Name = auto +Module = auto + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Automatically detect site changes, rebuild and optionally refresh a browser. diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py new file mode 100644 index 0000000..cb726d9 --- /dev/null +++ b/nikola/plugins/command/auto.py @@ -0,0 +1,103 @@ +# -*- 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, unicode_literals + +import codecs +import json +import os +import subprocess + +from nikola.plugin_categories import Command +from nikola.utils import req_missing + +GUARDFILE = """#!/usr/bin/env python +# -*- coding: utf-8 -*- +from livereload.task import Task +import json +import subprocess + +def f(): + import subprocess + subprocess.call(("nikola", "build")) + +fdata = json.loads('''{0}''') + +for watch in fdata: + Task.add(watch, f) +""" + + +class Auto(Command): + """Start debugging console.""" + name = "auto" + doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser" + cmd_options = [ + { + 'name': 'browser', + 'short': 'b', + 'type': bool, + 'help': 'Start a web browser.', + 'default': False, + }, + { + 'name': 'port', + 'short': 'p', + 'long': 'port', + 'default': 8000, + 'type': int, + 'help': 'Port nummber (default: 8000)', + }, + ] + + def _execute(self, options, args): + """Start the watcher.""" + try: + from livereload.server import start + except ImportError: + req_missing(['livereload'], 'use the "auto" command') + return + + # Run an initial build so we are uptodate + subprocess.call(("nikola", "build")) + + port = options and options.get('port') + + # Create a Guardfile + with codecs.open("Guardfile", "wb+", "utf8") as guardfile: + l = ["conf.py", "themes", "templates", self.site.config['GALLERY_PATH']] + for item in self.site.config['post_pages']: + l.append(os.path.dirname(item[0])) + for item in self.site.config['FILES_FOLDERS']: + l.append(os.path.dirname(item)) + data = GUARDFILE.format(json.dumps(l)) + guardfile.write(data) + + out_folder = self.site.config['OUTPUT_FOLDER'] + + os.chmod("Guardfile", 0o755) + + start(port, out_folder, options and options.get('browser')) diff --git a/nikola/plugins/command_bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin index f75f734..7091310 100644 --- a/nikola/plugins/command_bootswatch_theme.plugin +++ b/nikola/plugins/command/bootswatch_theme.plugin @@ -1,10 +1,10 @@ [Core] Name = bootswatch_theme -Module = command_bootswatch_theme +Module = bootswatch_theme [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Given a swatch name and a parent theme, creates a custom theme. diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py index 8400c9f..eb27f94 100644 --- a/nikola/plugins/command_bootswatch_theme.py +++ b/nikola/plugins/command/bootswatch_theme.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -31,15 +33,18 @@ except ImportError: requests = None # NOQA from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER) class CommandBootswatchTheme(Command): - """Given a swatch name and a parent theme, creates a custom theme.""" + """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" name = "bootswatch_theme" doc_usage = "[options]" - doc_purpose = "Given a swatch name and a parent theme, creates a custom"\ - " theme." + doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\ + " theme" cmd_options = [ { 'name': 'name', @@ -60,37 +65,39 @@ class CommandBootswatchTheme(Command): 'name': 'parent', 'short': 'p', 'long': 'parent', - 'default': 'site', - 'help': 'Parent theme name (default: site)', + 'default': 'bootstrap3', + 'help': 'Parent theme name (default: bootstrap3)', }, ] def _execute(self, options, args): """Given a swatch name and a parent theme, creates a custom theme.""" if requests is None: - print('To use the install_theme command, you need to install the ' - '"requests" package.') - return + utils.req_missing(['requests'], 'install Bootswatch themes') name = options['name'] swatch = options['swatch'] parent = options['parent'] + version = '' + + # See if we need bootswatch for bootstrap v2 or v3 + themes = utils.get_theme_chain(parent) + if 'bootstrap3' not in themes: + version = '2' + elif 'bootstrap' not in themes: + LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap') - print("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, - parent)) - try: - os.makedirs(os.path.join('themes', name, 'assets', 'css')) - except: - pass + LOGGER.notice("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent)) + utils.makedirs(os.path.join('themes', name, 'assets', 'css')) for fname in ('bootstrap.min.css', 'bootstrap.css'): - url = '/'.join(('http://bootswatch.com', swatch, fname)) - print("Downloading: ", url) + url = '/'.join(('http://bootswatch.com', version, swatch, fname)) + LOGGER.notice("Downloading: " + url) data = requests.get(url).text with open(os.path.join('themes', name, 'assets', 'css', fname), 'wb+') as output: - output.write(data) + output.write(data.encode('utf-8')) with open(os.path.join('themes', name, 'parent'), 'wb+') as output: - output.write(parent) - print('Theme created. Change the THEME setting to "{0}" to use ' - 'it.'.format(name)) + output.write(parent.encode('utf-8')) + LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use ' + 'it.'.format(name)) diff --git a/nikola/plugins/command_check.plugin b/nikola/plugins/command/check.plugin index d4dcd1c..8ceda5f 100644 --- a/nikola/plugins/command_check.plugin +++ b/nikola/plugins/command/check.plugin @@ -1,10 +1,10 @@ [Core] Name = check -Module = command_check +Module = check [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Check the generated site diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py new file mode 100644 index 0000000..5c7e49a --- /dev/null +++ b/nikola/plugins/command/check.py @@ -0,0 +1,204 @@ +# -*- 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 +import os +import re +import sys +try: + from urllib import unquote + from urlparse import urlparse +except ImportError: + from urllib.parse import unquote, urlparse # NOQA + +import lxml.html + +from nikola.plugin_categories import Command +from nikola.utils import get_logger + + +class CommandCheck(Command): + """Check the generated site.""" + + name = "check" + logger = None + + doc_usage = "-l [--find-sources] | -f" + doc_purpose = "check links and files in the generated site" + cmd_options = [ + { + 'name': 'links', + 'short': 'l', + 'long': 'check-links', + 'type': bool, + 'default': False, + 'help': 'Check for dangling links', + }, + { + 'name': 'files', + 'short': 'f', + 'long': 'check-files', + 'type': bool, + 'default': False, + 'help': 'Check for unknown files', + }, + { + 'name': 'clean', + 'long': 'clean-files', + 'type': bool, + 'default': False, + 'help': 'Remove all unknown files, use with caution', + }, + { + 'name': 'find_sources', + 'long': 'find-sources', + 'type': bool, + 'default': False, + 'help': 'List possible source files for files with broken links.', + }, + ] + + def _execute(self, options, args): + """Check the generated site.""" + + self.logger = get_logger('check', self.site.loghandlers) + + if not options['links'] and not options['files'] and not options['clean']: + print(self.help()) + return False + if options['links']: + failure = self.scan_links(options['find_sources']) + if options['files']: + failure = self.scan_files() + if options['clean']: + failure = self.clean_files() + if failure: + sys.exit(1) + + existing_targets = set([]) + + def analyze(self, task, find_sources=False): + rv = False + self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']] + try: + filename = task.split(":")[-1] + d = lxml.html.fromstring(open(filename).read()) + for l in d.iterlinks(): + target = l[0].attrib[l[1]] + if target == "#": + continue + parsed = urlparse(target) + if parsed.scheme or target.startswith('//'): + continue + if parsed.fragment: + target = target.split('#')[0] + target_filename = os.path.abspath( + os.path.join(os.path.dirname(filename), unquote(target))) + if any(re.match(x, target_filename) for x in self.whitelist): + continue + elif target_filename not in self.existing_targets: + if os.path.exists(target_filename): + self.existing_targets.add(target_filename) + else: + rv = True + self.logger.warn("Broken link in {0}: ".format(filename), target) + if find_sources: + self.logger.warn("Possible sources:") + self.logger.warn(os.popen('nikola list --deps ' + task, 'r').read()) + self.logger.warn("===============================\n") + except Exception as exc: + self.logger.error("Error with:", filename, exc) + return rv + + def scan_links(self, find_sources=False): + self.logger.notice("Checking Links:") + self.logger.notice("===============") + failure = False + for task in os.popen('nikola list --all', 'r').readlines(): + task = task.strip() + if task.split(':')[0] in ( + 'render_tags', 'render_archive', + 'render_galleries', 'render_indexes', + 'render_pages' + 'render_site') and '.html' in task: + if self.analyze(task, find_sources): + failure = True + if not failure: + self.logger.notice("All links checked.") + return failure + + def scan_files(self): + failure = False + self.logger.notice("Checking Files:") + self.logger.notice("===============\n") + only_on_output, only_on_input = self.real_scan_files() + + # Ignore folders + only_on_output = [p for p in only_on_output if not os.path.isdir(p)] + only_on_input = [p for p in only_on_input if not os.path.isdir(p)] + + if only_on_output: + only_on_output.sort() + self.logger.warn("Files from unknown origins:") + for f in only_on_output: + self.logger.warn(f) + failure = True + if only_on_input: + only_on_input.sort() + self.logger.warn("Files not generated:") + for f in only_on_input: + self.logger.warn(f) + if not failure: + self.logger.notice("All files checked.") + return failure + + def clean_files(self): + only_on_output, _ = self.real_scan_files() + for f in only_on_output: + os.unlink(f) + return True + + def real_scan_files(self): + task_fnames = set([]) + real_fnames = set([]) + output_folder = self.site.config['OUTPUT_FOLDER'] + # First check that all targets are generated in the right places + for task in os.popen('nikola list --all', 'r').readlines(): + task = task.strip() + if output_folder in task and ':' in task: + fname = task.split(':', 1)[-1] + task_fnames.add(fname) + # And now check that there are no non-target files + for root, dirs, files in os.walk(output_folder): + for src_name in files: + fname = os.path.join(root, src_name) + real_fnames.add(fname) + + only_on_output = list(real_fnames - task_fnames) + + only_on_input = list(task_fnames - real_fnames) + + return (only_on_output, only_on_input) diff --git a/nikola/plugins/command_console.plugin b/nikola/plugins/command/console.plugin index 003b994..a2be9ca 100644 --- a/nikola/plugins/command_console.plugin +++ b/nikola/plugins/command/console.plugin @@ -1,9 +1,9 @@ [Core] Name = console -Module = command_console +Module = console [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Start a debugging python console diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command/console.py index f4d0295..fe17dfc 100644 --- a/nikola/plugins/command_console.py +++ b/nikola/plugins/command/console.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,14 +28,19 @@ from __future__ import print_function, unicode_literals import os +from nikola import __version__ from nikola.plugin_categories import Command +from nikola.utils import get_logger, STDERR_HANDLER + +LOGGER = get_logger('console', STDERR_HANDLER) class Console(Command): """Start debugging console.""" name = "console" shells = ['ipython', 'bpython', 'plain'] - doc_purpose = "Start an interactive python console with access to your site and configuration." + doc_purpose = "Start an interactive Python (IPython->bpython->plain) console with access to your site and configuration" + header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)" def ipython(self): """IPython shell.""" @@ -41,13 +48,12 @@ class Console(Command): try: import conf except ImportError: - print("No configuration found, cannot run the console.") + LOGGER.error("No configuration found, cannot run the console.") else: import IPython SITE = Nikola(**conf.__dict__) SITE.scan_posts() - IPython.embed(header='Nikola Console (conf = configuration, SITE ' - '= site engine)') + IPython.embed(header=self.header.format('IPython')) def bpython(self): """bpython shell.""" @@ -55,14 +61,14 @@ class Console(Command): try: import conf except ImportError: - print("No configuration found, cannot run the console.") + LOGGER.error("No configuration found, cannot run the console.") else: import bpython SITE = Nikola(**conf.__dict__) SITE.scan_posts() gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} - bpython.embed(banner='Nikola Console (conf = configuration, SITE ' - '= site engine)', locals_=gl) + bpython.embed(banner=self.header.format( + 'bpython (Slightly Deprecated)'), locals_=gl) def plain(self): """Plain Python shell.""" @@ -73,7 +79,7 @@ class Console(Command): SITE.scan_posts() gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} except ImportError: - print("No configuration found, cannot run the console.") + LOGGER.error("No configuration found, cannot run the console.") else: import code try: @@ -92,8 +98,7 @@ class Console(Command): except NameError: pass - code.interact(local=gl, banner='Nikola Console (conf = ' - 'configuration, SITE = site engine)') + code.interact(local=gl, banner=self.header.format('Python')) def _execute(self, options, args): """Start the console.""" diff --git a/nikola/plugins/command_deploy.plugin b/nikola/plugins/command/deploy.plugin index c8776b5..10cc796 100644 --- a/nikola/plugins/command_deploy.plugin +++ b/nikola/plugins/command/deploy.plugin @@ -1,9 +1,9 @@ [Core] Name = deploy -Module = command_deploy +Module = deploy [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Deploy the site diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py new file mode 100644 index 0000000..efb909d --- /dev/null +++ b/nikola/plugins/command/deploy.py @@ -0,0 +1,141 @@ +# -*- 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 +from ast import literal_eval +import codecs +from datetime import datetime +import os +import sys +import subprocess +import time +import pytz + +from blinker import signal + +from nikola.plugin_categories import Command +from nikola.utils import remove_file, get_logger + + +class Deploy(Command): + """Deploy site. """ + name = "deploy" + + doc_usage = "" + doc_purpose = "deploy the site" + + logger = None + + def _execute(self, command, args): + self.logger = get_logger('deploy', self.site.loghandlers) + # Get last successful deploy date + timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy') + if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo': + self.logger.warn("\nWARNING WARNING WARNING WARNING\n" + "You are deploying using the nikolademo Disqus account.\n" + "That means you will not be able to moderate the comments in your own site.\n" + "And is probably not what you want to do.\n" + "Think about it for 5 seconds, I'll wait :-)\n\n") + time.sleep(5) + + deploy_drafts = self.site.config.get('DEPLOY_DRAFTS', True) + deploy_future = self.site.config.get('DEPLOY_FUTURE', False) + if not (deploy_drafts and deploy_future): + # Remove drafts and future posts + out_dir = self.site.config['OUTPUT_FOLDER'] + undeployed_posts = [] + self.site.scan_posts() + for post in self.site.timeline: + if (not deploy_drafts and post.is_draft) or \ + (not deploy_future and post.publish_later): + remove_file(os.path.join(out_dir, post.destination_path())) + remove_file(os.path.join(out_dir, post.source_path)) + undeployed_posts.append(post) + + for command in self.site.config['DEPLOY_COMMANDS']: + self.logger.notice("==> {0}".format(command)) + try: + subprocess.check_call(command, shell=True) + except subprocess.CalledProcessError as e: + self.logger.error('Failed deployment — command {0} ' + 'returned {1}'.format(e.cmd, e.returncode)) + sys.exit(e.returncode) + + self.logger.notice("Successful deployment") + if self.site.config['TIMEZONE'] is not None: + tzinfo = pytz.timezone(self.site.config['TIMEZONE']) + else: + tzinfo = pytz.UTC + try: + with open(timestamp_path, 'rb') as inf: + last_deploy = literal_eval(inf.read().strip()) + # this might ignore DST + last_deploy = last_deploy.replace(tzinfo=tzinfo) + clean = False + except Exception: + last_deploy = datetime(1970, 1, 1).replace(tzinfo=tzinfo) + clean = True + + new_deploy = datetime.now() + self._emit_deploy_event(last_deploy, new_deploy, clean, undeployed_posts) + + # Store timestamp of successful deployment + with codecs.open(timestamp_path, 'wb+', 'utf8') as outf: + outf.write(repr(new_deploy)) + + def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None): + """ Emit events for all timeline entries newer than last deploy. + + last_deploy: datetime + Time stamp of the last successful deployment. + + new_deploy: datetime + Time stamp of the current deployment. + + clean: bool + True when it appears like deploy is being run after a clean. + + """ + + if undeployed is None: + undeployed = [] + + event = { + 'last_deploy': last_deploy, + 'new_deploy': new_deploy, + 'clean': clean, + 'undeployed': undeployed + } + + deployed = [ + entry for entry in self.site.timeline + if entry.date > last_deploy and entry not in undeployed + ] + + event['deployed'] = deployed + + if len(deployed) > 0 or len(undeployed) > 0: + signal('deployed').send(event) diff --git a/nikola/plugins/command_import_blogger.plugin b/nikola/plugins/command/import_blogger.plugin index b275a7f..91a7cb6 100644 --- a/nikola/plugins/command_import_blogger.plugin +++ b/nikola/plugins/command/import_blogger.plugin @@ -1,10 +1,10 @@ [Core] Name = import_blogger -Module = command_import_blogger +Module = import_blogger [Documentation] Author = Roberto Alsina Version = 0.2 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Import a blogger site from a XML dump. diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command/import_blogger.py index ecc4676..53618b4 100644 --- a/nikola/plugins/command_import_blogger.py +++ b/nikola/plugins/command/import_blogger.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -23,8 +25,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals, print_function -import codecs -import csv import datetime import os import time @@ -38,30 +38,23 @@ try: import feedparser except ImportError: feedparser = None # NOQA -from lxml import html -from mako.template import Template from nikola.plugin_categories import Command from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin -links = {} +LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER) -class CommandImportBlogger(Command): +class CommandImportBlogger(Command, ImportMixin): """Import a blogger dump.""" name = "import_blogger" needs_config = False doc_usage = "[options] blogger_export_file" - doc_purpose = "Import a blogger dump." - cmd_options = [ - { - 'name': 'output_folder', - 'long': 'output-folder', - 'short': 'o', - 'default': 'new_site', - 'help': 'Location to write imported content.' - }, + doc_purpose = "import a blogger dump" + cmd_options = ImportMixin.cmd_options + [ { 'name': 'exclude_drafts', 'long': 'no-drafts', @@ -74,11 +67,9 @@ class CommandImportBlogger(Command): def _execute(self, options, args): """Import a Blogger blog from an export file into a Nikola site.""" - # Parse the data if feedparser is None: - print('To use the import_blogger command,' - ' you have to install the "feedparser" package.') + req_missing(['feedparser'], 'import Blogger dumps') return if not args: @@ -101,8 +92,11 @@ class CommandImportBlogger(Command): self.write_urlmap_csv( os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - self.write_configuration(self.get_configuration_output_path( - ), conf_template.render(**self.context)) + conf_out_path = self.get_configuration_output_path() + # if it tracebacks here, look a comment in + # basic_import.Import_Mixin.generate_base_site + conf_termplate_render = conf_template.render(**self.context) + self.write_configuration(conf_out_path, conf_termplate_render) @classmethod def get_channel_from_file(cls, filename): @@ -111,82 +105,36 @@ class CommandImportBlogger(Command): return feedparser.parse(filename) @staticmethod - def configure_redirections(url_map): - redirections = [] - for k, v in url_map.items(): - # remove the initial "/" because src is a relative file path - src = (urlparse(k).path + 'index.html')[1:] - dst = (urlparse(v).path) - if src == 'index.html': - print("Can't do a redirect for: {0!r}".format(k)) - else: - redirections.append((src, dst)) - - return redirections - - def generate_base_site(self): - if not os.path.exists(self.output_folder): - os.system('nikola init ' + self.output_folder) - else: - self.import_into_existing_site = True - print('The folder {0} already exists - assuming that this is a ' - 'already existing nikola site.'.format(self.output_folder)) - - conf_template = Template(filename=os.path.join( - os.path.dirname(utils.__file__), 'conf.py.in')) - - return conf_template - - @staticmethod def populate_context(channel): + # may need changes when the template conf.py.in changes context = {} context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language # in the dump context['BLOG_TITLE'] = channel.feed.title context['BLOG_DESCRIPTION'] = '' # Missing in the dump - context['SITE_URL'] = channel.feed.link.rstrip('/') + context['SITE_URL'] = channel.feed.link context['BLOG_EMAIL'] = channel.feed.author_detail.email context['BLOG_AUTHOR'] = channel.feed.author_detail.name - context['POST_PAGES'] = '''( - ("posts/*.html", "posts", "post.tmpl", True), - ("stories/*.html", "stories", "story.tmpl", False), - )''' - context['POST_COMPILERS'] = '''{ - "rest": ('.txt', '.rst'), - "markdown": ('.md', '.mdown', '.markdown', '.wp'), - "html": ('.html', '.htm') - } - ''' + context['POSTS'] = '''( + ("posts/*.txt", "posts", "post.tmpl"), + ("posts/*.rst", "posts", "post.tmpl"), + ("posts/*.html", "posts", "post.tmpl"), + )''' + context['PAGES'] = '''( + ("articles/*.txt", "articles", "story.tmpl"), + ("articles/*.rst", "articles", "story.tmpl"), + )''' + context['COMPILERS'] = '''{ + "rest": ('.txt', '.rst'), + "markdown": ('.md', '.mdown', '.markdown', '.wp'), + "html": ('.html', '.htm') + } + ''' + context['THEME'] = 'bootstrap3' return context - @classmethod - def transform_content(cls, content): - # No transformations yet - return content - - @classmethod - def write_content(cls, filename, content): - doc = html.document_fromstring(content) - doc.rewrite_links(replacer) - - with open(filename, "wb+") as fd: - fd.write(html.tostring(doc, encoding='utf8')) - - @staticmethod - def write_metadata(filename, title, slug, post_date, description, tags): - if not description: - description = "" - - with codecs.open(filename, "w+", "utf8") as fd: - fd.write('{0}\n'.format(title)) - fd.write('{0}\n'.format(slug)) - fd.write('{0}\n'.format(post_date)) - fd.write('{0}\n'.format(','.join(tags))) - fd.write('\n') - fd.write('{0}\n'.format(description)) - def import_item(self, item, out_folder=None): """Takes an item from the feed and creates a post file.""" if out_folder is None: @@ -201,8 +149,8 @@ class CommandImportBlogger(Command): # blogger supports empty titles, which Nikola doesn't if not title: - print("Warning: Empty title in post with URL {0}. Using NO_TITLE " - "as placeholder, please fix.".format(link)) + LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE " + "as placeholder, please fix.".format(link)) title = "NO_TITLE" if link_path.lower().endswith('.html'): @@ -211,7 +159,7 @@ class CommandImportBlogger(Command): slug = utils.slugify(link_path) if not slug: # should never happen - print("Error converting post:", title) + LOGGER.error("Error converting post:", title) return description = '' @@ -239,7 +187,7 @@ class CommandImportBlogger(Command): out_folder + '/' + slug + '.html' if is_draft and self.exclude_drafts: - print('Draft "{0}" will not be imported.'.format(title)) + LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) elif content.strip(): # If no content is found, no files are written. content = self.transform_content(content) @@ -251,8 +199,8 @@ class CommandImportBlogger(Command): os.path.join(self.output_folder, out_folder, slug + '.html'), content) else: - print('Not going to import "{0}" because it seems to contain' - ' no content.'.format(title)) + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) def process_item(self, item): post_type = item.tags[0].term @@ -274,35 +222,8 @@ class CommandImportBlogger(Command): # FIXME: not importing comments. Does blogger support "pages"? pass else: - print("Unknown post_type:", post_type) + LOGGER.warn("Unknown post_type:", post_type) def import_posts(self, channel): for item in channel.entries: self.process_item(item) - - @staticmethod - def write_urlmap_csv(output_file, url_map): - with codecs.open(output_file, 'w+', 'utf8') as fd: - csv_writer = csv.writer(fd) - for item in url_map.items(): - csv_writer.writerow(item) - - def get_configuration_output_path(self): - if not self.import_into_existing_site: - filename = 'conf.py' - else: - filename = 'conf.py.blogger_import-{0}'.format( - datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) - config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to: ' + config_output_path) - - return config_output_path - - @staticmethod - def write_configuration(filename, rendered_template): - with codecs.open(filename, 'w+', 'utf8') as fd: - fd.write(rendered_template) - - -def replacer(dst): - return links.get(dst, dst) diff --git a/nikola/plugins/command/import_feed.plugin b/nikola/plugins/command/import_feed.plugin new file mode 100644 index 0000000..26e570a --- /dev/null +++ b/nikola/plugins/command/import_feed.plugin @@ -0,0 +1,10 @@ +[Core] +Name = import_feed +Module = import_feed + +[Documentation] +Author = Grzegorz Śliwiński +Version = 0.1 +Website = http://www.fizyk.net.pl/ +Description = Import a blog posts from a RSS/Atom dump + diff --git a/nikola/plugins/command/import_feed.py b/nikola/plugins/command/import_feed.py new file mode 100644 index 0000000..b25d9ec --- /dev/null +++ b/nikola/plugins/command/import_feed.py @@ -0,0 +1,197 @@ +# -*- 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 datetime +import os +import time + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # NOQA + +try: + import feedparser +except ImportError: + feedparser = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin + +LOGGER = utils.get_logger('import_feed', utils.STDERR_HANDLER) + + +class CommandImportFeed(Command, ImportMixin): + """Import a feed dump.""" + + name = "import_feed" + needs_config = False + doc_usage = "[options] feed_file" + doc_purpose = "import a RSS/Atom dump" + cmd_options = ImportMixin.cmd_options + + def _execute(self, options, args): + ''' + Import Atom/RSS feed + ''' + if feedparser is None: + req_missing(['feedparser'], 'import feeds') + return + + if not args: + print(self.help()) + return + + options['filename'] = args[0] + self.feed_export_file = options['filename'] + self.output_folder = options['output_folder'] + self.import_into_existing_site = False + self.url_map = {} + channel = self.get_channel_from_file(self.feed_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + self.context['REDIRECTIONS'] = self.configure_redirections( + self.url_map) + + self.import_posts(channel) + + self.write_configuration(self.get_configuration_output_path( + ), conf_template.render(**self.context)) + + @classmethod + def get_channel_from_file(cls, filename): + return feedparser.parse(filename) + + @staticmethod + def populate_context(channel): + context = {} + context['DEFAULT_LANG'] = channel.feed.title_detail.language \ + if channel.feed.title_detail.language else 'en' + context['BLOG_TITLE'] = channel.feed.title + + context['BLOG_DESCRIPTION'] = channel.feed.get('subtitle', '') + context['SITE_URL'] = channel.feed.get('link', '').rstrip('/') + context['BLOG_EMAIL'] = channel.feed.author_detail.get('email', '') if 'author_detail' in channel.feed else '' + context['BLOG_AUTHOR'] = channel.feed.author_detail.get('name', '') if 'author_detail' in channel.feed else '' + + context['POST_PAGES'] = '''( + ("posts/*.html", "posts", "post.tmpl", True), + ("stories/*.html", "stories", "story.tmpl", False), + )''' + context['COMPILERS'] = '''{ + "rest": ('.txt', '.rst'), + "markdown": ('.md', '.mdown', '.markdown', '.wp'), + "html": ('.html', '.htm') + } + ''' + + return context + + def import_posts(self, channel): + for item in channel.entries: + self.process_item(item) + + def process_item(self, item): + self.import_item(item, 'posts') + + def import_item(self, item, out_folder=None): + """Takes an item from the feed and creates a post file.""" + if out_folder is None: + out_folder = 'posts' + + # link is something like http://foo.com/2012/09/01/hello-world/ + # So, take the path, utils.slugify it, and that's our slug + link = item.link + link_path = urlparse(link).path + + title = item.title + + # blogger supports empty titles, which Nikola doesn't + if not title: + LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE " + "as placeholder, please fix.".format(link)) + title = "NO_TITLE" + + if link_path.lower().endswith('.html'): + link_path = link_path[:-5] + + slug = utils.slugify(link_path) + + if not slug: # should never happen + LOGGER.error("Error converting post:", title) + return + + description = '' + post_date = datetime.datetime.fromtimestamp(time.mktime( + item.published_parsed)) + if item.get('content'): + for candidate in item.get('content', []): + content = candidate.value + break + # FIXME: handle attachments + elif item.get('summary'): + content = item.get('summary') + + tags = [] + for tag in item.get('tags', []): + tags.append(tag.term) + + if item.get('app_draft'): + tags.append('draft') + is_draft = True + else: + is_draft = False + + self.url_map[link] = self.context['SITE_URL'] + '/' + \ + out_folder + '/' + slug + '.html' + + if is_draft and self.exclude_drafts: + LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) + elif content.strip(): + # If no content is found, no files are written. + content = self.transform_content(content) + + self.write_metadata(os.path.join(self.output_folder, out_folder, + slug + '.meta'), + title, slug, post_date, description, tags) + self.write_content( + os.path.join(self.output_folder, out_folder, slug + '.html'), + content) + else: + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) + + @staticmethod + def write_metadata(filename, title, slug, post_date, description, tags): + ImportMixin.write_metadata(filename, + title, + slug, + post_date.strftime(r'%Y/%m/%d %H:%m:%S'), + description, + tags) diff --git a/nikola/plugins/command_import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin index ff7cdca..fadc759 100644 --- a/nikola/plugins/command_import_wordpress.plugin +++ b/nikola/plugins/command/import_wordpress.plugin @@ -1,10 +1,10 @@ [Core] Name = import_wordpress -Module = command_import_wordpress +Module = import_wordpress [Documentation] Author = Roberto Alsina Version = 0.2 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Import a wordpress site from a XML dump (requires markdown). diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command/import_wordpress.py index b45fe78..4f32198 100644 --- a/nikola/plugins/command_import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -23,46 +25,43 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals, print_function -import codecs -import csv -import datetime import os import re +import sys +from lxml import etree try: from urlparse import urlparse + from urllib import unquote except ImportError: - from urllib.parse import urlparse # NOQA - -from lxml import etree, html -from mako.template import Template + from urllib.parse import urlparse, unquote # NOQA try: import requests except ImportError: requests = None # NOQA +try: + import phpserialize +except ImportError: + phpserialize = None # NOQA + from nikola.plugin_categories import Command from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin, links -links = {} +LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) -class CommandImportWordpress(Command): - """Import a wordpress dump.""" +class CommandImportWordpress(Command, ImportMixin): + """Import a WordPress dump.""" name = "import_wordpress" needs_config = False doc_usage = "[options] wordpress_export_file" - doc_purpose = "Import a wordpress dump." - cmd_options = [ - { - 'name': 'output_folder', - 'long': 'output-folder', - 'short': 'o', - 'default': 'new_site', - 'help': 'Location to write imported content.' - }, + doc_purpose = "import a WordPress dump" + cmd_options = ImportMixin.cmd_options + [ { 'name': 'exclude_drafts', 'long': 'no-drafts', @@ -88,13 +87,7 @@ class CommandImportWordpress(Command): ] def _execute(self, options={}, args=[]): - """Import a Wordpress blog from an export file into a Nikola site.""" - # Parse the data - if requests is None: - print('To use the import_wordpress command,' - ' you have to install the "requests" package.') - return - + """Import a WordPress blog from an export file into a Nikola site.""" if not args: print(self.help()) return @@ -106,17 +99,38 @@ class CommandImportWordpress(Command): options['output_folder'] = args.pop(0) if args: - print('You specified additional arguments ({0}). Please consider ' - 'putting these arguments before the filename if you ' - 'are running into problems.'.format(args)) + LOGGER.warn('You specified additional arguments ({0}). Please consider ' + 'putting these arguments before the filename if you ' + 'are running into problems.'.format(args)) + + self.import_into_existing_site = False + self.url_map = {} + self.timezone = None self.wordpress_export_file = options['filename'] self.squash_newlines = options.get('squash_newlines', False) - self.no_downloads = options.get('no_downloads', False) self.output_folder = options.get('output_folder', 'new_site') - self.import_into_existing_site = False + self.exclude_drafts = options.get('exclude_drafts', False) - self.url_map = {} + self.no_downloads = options.get('no_downloads', False) + + if not self.no_downloads: + def show_info_about_mising_module(modulename): + LOGGER.error( + 'To use the "{commandname}" command, you have to install ' + 'the "{package}" package or supply the "--no-downloads" ' + 'option.'.format( + commandname=self.name, + package=modulename) + ) + + if requests is None and phpserialize is None: + req_missing(['requests', 'phpserialize'], 'import WordPress dumps without --no-downloads') + elif requests is None: + req_missing(['requests'], 'import WordPress dumps without --no-downloads') + elif phpserialize is None: + req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads') + channel = self.get_channel_from_file(self.wordpress_export_file) self.context = self.populate_context(channel) conf_template = self.generate_base_site() @@ -130,6 +144,10 @@ class CommandImportWordpress(Command): rendered_template = conf_template.render(**self.context) rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', rendered_template) + if self.timezone: + rendered_template = re.sub('# TIMEZONE = \'Europe/Zurich\'', + 'TIMEZONE = \'' + self.timezone + '\'', + rendered_template) self.write_configuration(self.get_configuration_output_path(), rendered_template) @@ -173,33 +191,6 @@ class CommandImportWordpress(Command): return channel @staticmethod - def configure_redirections(url_map): - redirections = [] - for k, v in url_map.items(): - # remove the initial "/" because src is a relative file path - src = (urlparse(k).path + 'index.html')[1:] - dst = (urlparse(v).path) - if src == 'index.html': - print("Can't do a redirect for: {0!r}".format(k)) - else: - redirections.append((src, dst)) - - return redirections - - def generate_base_site(self): - if not os.path.exists(self.output_folder): - os.system('nikola init ' + self.output_folder) - else: - self.import_into_existing_site = True - print('The folder {0} already exists - assuming that this is a ' - 'already existing nikola site.'.format(self.output_folder)) - - conf_template = Template(filename=os.path.join( - os.path.dirname(utils.__file__), 'conf.py.in')) - - return conf_template - - @staticmethod def populate_context(channel): wordpress_namespace = channel.nsmap['wp'] @@ -212,8 +203,11 @@ class CommandImportWordpress(Command): context['BASE_URL'] = get_text_tag(channel, 'link', '#') if not context['BASE_URL']: base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace)) - context['BASE_URL'] = get_text_tag(base_site_url, None, "http://foo.com") + context['BASE_URL'] = get_text_tag(base_site_url, + None, + "http://foo.com") context['SITE_URL'] = context['BASE_URL'] + context['THEME'] = 'bootstrap3' author = channel.find('{{{0}}}author'.format(wordpress_namespace)) context['BLOG_EMAIL'] = get_text_tag( @@ -224,11 +218,13 @@ class CommandImportWordpress(Command): author, '{{{0}}}author_display_name'.format(wordpress_namespace), "Joe Example") - context['POST_PAGES'] = '''( - ("posts/*.wp", "posts", "post.tmpl", True), - ("stories/*.wp", "stories", "story.tmpl", False), + context['POSTS'] = '''( + ("posts/*.wp", "posts", "post.tmpl"), )''' - context['POST_COMPILERS'] = '''{ + context['PAGES'] = '''( + ("stories/*.wp", "stories", "story.tmpl"), + )''' + context['COMPILERS'] = '''{ "rest": ('.txt', '.rst'), "markdown": ('.md', '.mdown', '.markdown', '.wp'), "html": ('.html', '.htm') @@ -245,8 +241,7 @@ class CommandImportWordpress(Command): with open(dst_path, 'wb+') as fd: fd.write(requests.get(url).content) except requests.exceptions.ConnectionError as err: - print("Downloading {0} to {1} failed: {2}".format(url, dst_path, - err)) + LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err)) def import_attachment(self, item, wordpress_namespace): url = get_text_tag( @@ -257,14 +252,67 @@ class CommandImportWordpress(Command): dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) dst_dir = os.path.dirname(dst_path) - if not os.path.isdir(dst_dir): - os.makedirs(dst_dir) - print("Downloading {0} => {1}".format(url, dst_path)) + utils.makedirs(dst_dir) + LOGGER.notice("Downloading {0} => {1}".format(url, dst_path)) self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[link] = '/' + dst_url links[url] = '/' + dst_url + self.download_additional_image_sizes( + item, + wordpress_namespace, + os.path.dirname(url) + ) + + def download_additional_image_sizes(self, item, wordpress_namespace, source_path): + if phpserialize is None: + return + + additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace)) + + if additional_metadata is None: + return + + for element in additional_metadata: + meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace)) + if meta_key is not None and meta_key.text == '_wp_attachment_metadata': + meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace)) + + if meta_value is None: + continue + + # Someone from Wordpress thought it was a good idea + # serialize PHP objects into that metadata field. Given + # that the export should give you the power to insert + # your blogging into another site or system its not. + # Why don't they just use JSON? + if sys.version_info[0] == 2: + metadata = phpserialize.loads(meta_value.text) + size_key = 'sizes' + file_key = 'file' + else: + metadata = phpserialize.loads(meta_value.text.encode('UTF-8')) + size_key = b'sizes' + file_key = b'file' + + if not size_key in metadata: + continue + + for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]: + url = '/'.join([source_path, filename.decode('utf-8')]) + + path = urlparse(url).path + dst_path = os.path.join(*([self.output_folder, 'files'] + + list(path.split('/')))) + dst_dir = os.path.dirname(dst_path) + utils.makedirs(dst_dir) + LOGGER.notice("Downloading {0} => {1}".format(url, dst_path)) + self.download_url_content_to_file(url, dst_path) + dst_url = '/'.join(dst_path.split(os.sep)[2:]) + links[url] = '/' + dst_url + links[url] = '/' + dst_url + @staticmethod def transform_sourcecode(content): new_content = re.sub('\[sourcecode language="([^"]+)"\]', @@ -293,27 +341,6 @@ class CommandImportWordpress(Command): new_content = self.transform_multiple_newlines(new_content) return new_content - @classmethod - def write_content(cls, filename, content): - doc = html.document_fromstring(content) - doc.rewrite_links(replacer) - - with open(filename, "wb+") as fd: - fd.write(html.tostring(doc, encoding='utf8')) - - @staticmethod - def write_metadata(filename, title, slug, post_date, description, tags): - if not description: - description = "" - - with codecs.open(filename, "w+", "utf8") as fd: - fd.write('{0}\n'.format(title)) - fd.write('{0}\n'.format(slug)) - fd.write('{0}\n'.format(post_date)) - fd.write('{0}\n'.format(','.join(tags))) - fd.write('\n') - fd.write('{0}\n'.format(description)) - def import_item(self, item, wordpress_namespace, out_folder=None): """Takes an item from the feed and creates a post file.""" if out_folder is None: @@ -323,12 +350,13 @@ class CommandImportWordpress(Command): # link is something like http://foo.com/2012/09/01/hello-world/ # So, take the path, utils.slugify it, and that's our slug link = get_text_tag(item, 'link', None) - path = urlparse(link).path + path = unquote(urlparse(link).path) # In python 2, path is a str. slug requires a unicode - # object. Luckily, paths are also ASCII + # object. According to wikipedia, unquoted strings will + # usually be UTF8 if isinstance(path, utils.bytes_str): - path = path.decode('ASCII') + path = path.decode('utf8') slug = utils.slugify(path) if not slug: # it happens if the post has no "nice" URL slug = get_text_tag( @@ -337,12 +365,15 @@ class CommandImportWordpress(Command): slug = get_text_tag( item, '{{{0}}}post_id'.format(wordpress_namespace), None) if not slug: # should never happen - print("Error converting post:", title) + LOGGER.error("Error converting post:", title) return description = get_text_tag(item, 'description', '') post_date = get_text_tag( item, '{{{0}}}post_date'.format(wordpress_namespace), None) + dt = utils.to_datetime(post_date) + if dt.tzinfo and self.timezone is None: + self.timezone = utils.get_tzname(dt) status = get_text_tag( item, '{{{0}}}status'.format(wordpress_namespace), 'publish') content = get_text_tag( @@ -350,7 +381,7 @@ class CommandImportWordpress(Command): tags = [] if status == 'trash': - print('Trashed post "{0}" will not be imported.'.format(title)) + LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title)) return elif status != 'publish': tags.append('draft') @@ -365,7 +396,7 @@ class CommandImportWordpress(Command): tags.append(text) if is_draft and self.exclude_drafts: - print('Draft "{0}" will not be imported.'.format(title)) + LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) elif content.strip(): # If no content is found, no files are written. self.url_map[link] = self.context['SITE_URL'] + '/' + \ @@ -380,8 +411,8 @@ class CommandImportWordpress(Command): os.path.join(self.output_folder, out_folder, slug + '.wp'), content) else: - print('Not going to import "{0}" because it seems to contain' - ' no content.'.format(title)) + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) def process_item(self, item): # The namespace usually is something like: @@ -401,33 +432,6 @@ class CommandImportWordpress(Command): for item in channel.findall('item'): self.process_item(item) - @staticmethod - def write_urlmap_csv(output_file, url_map): - with codecs.open(output_file, 'w+', 'utf8') as fd: - csv_writer = csv.writer(fd) - for item in url_map.items(): - csv_writer.writerow(item) - - def get_configuration_output_path(self): - if not self.import_into_existing_site: - filename = 'conf.py' - else: - filename = 'conf.py.wordpress_import-{0}'.format( - datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) - config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to:', config_output_path) - - return config_output_path - - @staticmethod - def write_configuration(filename, rendered_template): - with codecs.open(filename, 'w+', 'utf8') as fd: - fd.write(rendered_template) - - -def replacer(dst): - return links.get(dst, dst) - def get_text_tag(tag, name, default): if tag is None: diff --git a/nikola/plugins/command_init.plugin b/nikola/plugins/command/init.plugin index f4adf4a..a539f51 100644 --- a/nikola/plugins/command_init.plugin +++ b/nikola/plugins/command/init.plugin @@ -1,9 +1,9 @@ [Core] Name = init -Module = command_init +Module = init [Documentation] Author = Roberto Alsina Version = 0.2 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Create a new site. diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command/init.py index bc36266..1873ec4 100644 --- a/nikola/plugins/command_init.py +++ b/nikola/plugins/command/init.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -31,6 +33,10 @@ from mako.template import Template import nikola from nikola.plugin_categories import Command +from nikola.utils import get_logger, makedirs, STDERR_HANDLER +from nikola.winutils import fix_git_symlinked + +LOGGER = get_logger('init', STDERR_HANDLER) class CommandInit(Command): @@ -40,7 +46,7 @@ class CommandInit(Command): doc_usage = "[--demo] folder" needs_config = False - doc_purpose = """Create a Nikola site in the specified folder.""" + doc_purpose = "create a Nikola site in the specified folder" cmd_options = [ { 'name': 'demo', @@ -54,25 +60,33 @@ class CommandInit(Command): SAMPLE_CONF = { 'BLOG_AUTHOR': "Your Name", 'BLOG_TITLE': "Demo Site", - 'SITE_URL': "http://nikola.ralsina.com.ar", + 'SITE_URL': "http://getnikola.com/", 'BLOG_EMAIL': "joe@demo.site", 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", 'DEFAULT_LANG': "en", + 'THEME': 'bootstrap3', - 'POST_PAGES': """( - ("posts/*.txt", "posts", "post.tmpl", True), - ("stories/*.txt", "stories", "story.tmpl", False), + 'POSTS': """( + ("posts/*.rst", "posts", "post.tmpl"), + ("posts/*.txt", "posts", "post.tmpl"), )""", - - 'POST_COMPILERS': """{ - "rest": ('.txt', '.rst'), + 'PAGES': """( + ("stories/*.rst", "stories", "story.tmpl"), + ("stories/*.txt", "stories", "story.tmpl"), +)""", + 'COMPILERS': """{ + "rest": ('.rst', '.txt'), "markdown": ('.md', '.mdown', '.markdown'), "textile": ('.textile',), "txt2tags": ('.t2t',), "bbcode": ('.bb',), "wiki": ('.wiki',), "ipynb": ('.ipynb',), - "html": ('.html', '.htm') + "html": ('.html', '.htm'), + # Pandoc detects the input from the source filename + # but is disabled by default as it would conflict + # with many of the others. + # "pandoc": ('.rst', '.md', '.txt'), }""", 'REDIRECTIONS': '[]', } @@ -82,6 +96,7 @@ class CommandInit(Command): lib_path = cls.get_path_to_nikola_modules() src = os.path.join(lib_path, 'data', 'samplesite') shutil.copytree(src, target) + fix_git_symlinked(src, target) @classmethod def create_configuration(cls, target): @@ -95,7 +110,7 @@ class CommandInit(Command): @classmethod def create_empty_site(cls, target): for folder in ('files', 'galleries', 'listings', 'posts', 'stories'): - os.makedirs(os.path.join(target, folder)) + makedirs(os.path.join(target, folder)) @staticmethod def get_path_to_nikola_modules(): @@ -112,11 +127,11 @@ class CommandInit(Command): else: if not options or not options.get('demo'): self.create_empty_site(target) - print('Created empty site at {0}.'.format(target)) + LOGGER.notice('Created empty site at {0}.'.format(target)) else: self.copy_sample_site(target) - print("A new site with example data has been created at " - "{0}.".format(target)) - print("See README.txt in that folder for more information.") + LOGGER.notice("A new site with example data has been created at " + "{0}.".format(target)) + LOGGER.notice("See README.txt in that folder for more information.") self.create_configuration(target) diff --git a/nikola/plugins/command/install_plugin.plugin b/nikola/plugins/command/install_plugin.plugin new file mode 100644 index 0000000..3dbabd8 --- /dev/null +++ b/nikola/plugins/command/install_plugin.plugin @@ -0,0 +1,10 @@ +[Core] +Name = install_plugin +Module = install_plugin + +[Documentation] +Author = Roberto Alsina and Chris Warrick +Version = 0.1 +Website = http://getnikola.com +Description = Install a plugin into the current site. + diff --git a/nikola/plugins/command/install_plugin.py b/nikola/plugins/command/install_plugin.py new file mode 100644 index 0000000..fdbd0b7 --- /dev/null +++ b/nikola/plugins/command/install_plugin.py @@ -0,0 +1,185 @@ +# -*- 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 +import codecs +import os +import json +import shutil +import subprocess +from io import BytesIO + +import pygments +from pygments.lexers import PythonLexer +from pygments.formatters import TerminalFormatter + +try: + import requests +except ImportError: + requests = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('install_plugin', utils.STDERR_HANDLER) + + +# Stolen from textwrap in Python 3.3.2. +def indent(text, prefix, predicate=None): # NOQA + """Adds 'prefix' to the beginning of selected lines in 'text'. + + If 'predicate' is provided, 'prefix' will only be added to the lines + where 'predicate(line)' is True. If 'predicate' is not provided, + it will default to adding 'prefix' to all non-empty lines that do not + consist solely of whitespace characters. + """ + if predicate is None: + def predicate(line): + return line.strip() + + def prefixed_lines(): + for line in text.splitlines(True): + yield (prefix + line if predicate(line) else line) + return ''.join(prefixed_lines()) + + +class CommandInstallPlugin(Command): + """Install a plugin.""" + + name = "install_plugin" + doc_usage = "[[-u] plugin_name] | [[-u] -l]" + doc_purpose = "install plugin into current site" + output_dir = 'plugins' + cmd_options = [ + { + 'name': 'list', + 'short': 'l', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'Show list of available plugins.' + }, + { + 'name': 'url', + 'short': 'u', + 'long': 'url', + 'type': str, + 'help': "URL for the plugin repository (default: " + "http://plugins.getnikola.com/v6/plugins.json)", + 'default': 'http://plugins.getnikola.com/v6/plugins.json' + }, + ] + + def _execute(self, options, args): + """Install plugin into current site.""" + if requests is None: + utils.req_missing(['requests'], 'install plugins') + + listing = options['list'] + url = options['url'] + if args: + name = args[0] + else: + name = None + + if name is None and not listing: + LOGGER.error("This command needs either a plugin name or the -l option.") + return False + data = requests.get(url).text + data = json.loads(data) + if listing: + print("Plugins:") + print("--------") + for plugin in sorted(data.keys()): + print(plugin) + return True + else: + self.do_install(name, data) + + def do_install(self, name, data): + if name in data: + utils.makedirs(self.output_dir) + LOGGER.notice('Downloading: ' + data[name]) + zip_file = BytesIO() + zip_file.write(requests.get(data[name]).content) + LOGGER.notice('Extracting: {0} into plugins'.format(name)) + utils.extract_all(zip_file, 'plugins') + dest_path = os.path.join('plugins', name) + else: + try: + plugin_path = utils.get_plugin_path(name) + except: + LOGGER.error("Can't find plugin " + name) + return False + + utils.makedirs(self.output_dir) + dest_path = os.path.join(self.output_dir, name) + if os.path.exists(dest_path): + LOGGER.error("{0} is already installed".format(name)) + return False + + LOGGER.notice('Copying {0} into plugins'.format(plugin_path)) + shutil.copytree(plugin_path, dest_path) + + reqpath = os.path.join(dest_path, 'requirements.txt') + print(reqpath) + if os.path.exists(reqpath): + LOGGER.notice('This plugin has Python dependencies.') + LOGGER.notice('Installing dependencies with pip...') + try: + subprocess.check_call(('pip', 'install', '-r', reqpath)) + except subprocess.CalledProcessError: + LOGGER.error('Could not install the dependencies.') + print('Contents of the requirements.txt file:\n') + with codecs.open(reqpath, 'rb', 'utf-8') as fh: + print(indent(fh.read(), 4 * ' ')) + print('You have to install those yourself or through a ' + 'package manager.') + else: + LOGGER.notice('Dependency installation succeeded.') + reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt') + if os.path.exists(reqnpypath): + LOGGER.notice('This plugin has third-party ' + 'dependencies you need to install ' + 'manually.') + print('Contents of the requirements-nonpy.txt file:\n') + with codecs.open(reqnpypath, 'rb', 'utf-8') as fh: + for l in fh.readlines(): + i, j = l.split('::') + print(indent(i.strip(), 4 * ' ')) + print(indent(j.strip(), 8 * ' ')) + print() + + print('You have to install those yourself or through a package ' + 'manager.') + confpypath = os.path.join(dest_path, 'conf.py.sample') + if os.path.exists(confpypath): + LOGGER.notice('This plugin has a sample config file.') + print('Contents of the conf.py.sample file:\n') + with codecs.open(confpypath, 'rb', 'utf-8') as fh: + print(indent(pygments.highlight( + fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) + return True diff --git a/nikola/plugins/command_install_theme.plugin b/nikola/plugins/command/install_theme.plugin index f010074..84b2623 100644 --- a/nikola/plugins/command_install_theme.plugin +++ b/nikola/plugins/command/install_theme.plugin @@ -1,10 +1,10 @@ [Core] Name = install_theme -Module = command_install_theme +Module = install_theme [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Install a theme into the current site. diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py new file mode 100644 index 0000000..a9d835a --- /dev/null +++ b/nikola/plugins/command/install_theme.py @@ -0,0 +1,163 @@ +# -*- 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 +import os +import json +import shutil +import codecs +from io import BytesIO + +import pygments +from pygments.lexers import PythonLexer +from pygments.formatters import TerminalFormatter + +try: + import requests +except ImportError: + requests = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER) + + +# Stolen from textwrap in Python 3.3.2. +def indent(text, prefix, predicate=None): # NOQA + """Adds 'prefix' to the beginning of selected lines in 'text'. + + If 'predicate' is provided, 'prefix' will only be added to the lines + where 'predicate(line)' is True. If 'predicate' is not provided, + it will default to adding 'prefix' to all non-empty lines that do not + consist solely of whitespace characters. + """ + if predicate is None: + def predicate(line): + return line.strip() + + def prefixed_lines(): + for line in text.splitlines(True): + yield (prefix + line if predicate(line) else line) + return ''.join(prefixed_lines()) + + +class CommandInstallTheme(Command): + """Install a theme.""" + + name = "install_theme" + doc_usage = "[[-u] theme_name] | [[-u] -l]" + doc_purpose = "install theme into current site" + output_dir = 'themes' + cmd_options = [ + { + 'name': 'list', + 'short': 'l', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'Show list of available themes.' + }, + { + 'name': 'url', + 'short': 'u', + 'long': 'url', + 'type': str, + 'help': "URL for the theme repository (default: " + "http://themes.getnikola.com/v6/themes.json)", + 'default': 'http://themes.getnikola.com/v6/themes.json' + }, + ] + + def _execute(self, options, args): + """Install theme into current site.""" + if requests is None: + utils.req_missing(['requests'], 'install themes') + + listing = options['list'] + url = options['url'] + if args: + name = args[0] + else: + name = None + + if name is None and not listing: + LOGGER.error("This command needs either a theme name or the -l option.") + return False + data = requests.get(url).text + data = json.loads(data) + if listing: + print("Themes:") + print("-------") + for theme in sorted(data.keys()): + print(theme) + return True + else: + self.do_install(name, data) + # See if the theme's parent is available. If not, install it + while True: + parent_name = utils.get_parent_theme_name(name) + if parent_name is None: + break + try: + utils.get_theme_path(parent_name) + break + except: # Not available + self.do_install(parent_name, data) + name = parent_name + + def do_install(self, name, data): + if name in data: + utils.makedirs(self.output_dir) + LOGGER.notice('Downloading: ' + data[name]) + zip_file = BytesIO() + zip_file.write(requests.get(data[name]).content) + LOGGER.notice('Extracting: {0} into themes'.format(name)) + utils.extract_all(zip_file) + dest_path = os.path.join('themes', name) + else: + try: + theme_path = utils.get_theme_path(name) + except: + LOGGER.error("Can't find theme " + name) + return False + + utils.makedirs(self.output_dir) + dest_path = os.path.join(self.output_dir, name) + if os.path.exists(dest_path): + LOGGER.error("{0} is already installed".format(name)) + return False + + LOGGER.notice('Copying {0} into themes'.format(theme_path)) + shutil.copytree(theme_path, dest_path) + confpypath = os.path.join(dest_path, 'conf.py.sample') + if os.path.exists(confpypath): + LOGGER.notice('This plugin has a sample config file.') + print('Contents of the conf.py.sample file:\n') + with codecs.open(confpypath, 'rb', 'utf-8') as fh: + print(indent(pygments.highlight( + fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) + return True diff --git a/nikola/plugins/command/mincss.plugin b/nikola/plugins/command/mincss.plugin new file mode 100644 index 0000000..d394d06 --- /dev/null +++ b/nikola/plugins/command/mincss.plugin @@ -0,0 +1,10 @@ +[Core] +Name = mincss +Module = mincss + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Apply mincss to the generated site + diff --git a/nikola/plugins/command/mincss.py b/nikola/plugins/command/mincss.py new file mode 100644 index 0000000..5c9a7cb --- /dev/null +++ b/nikola/plugins/command/mincss.py @@ -0,0 +1,75 @@ +# -*- 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, unicode_literals +import os +import sys + +try: + from mincss.processor import Processor +except ImportError: + Processor = None + +from nikola.plugin_categories import Command +from nikola.utils import req_missing, get_logger, STDERR_HANDLER + + +class CommandMincss(Command): + """Check the generated site.""" + name = "mincss" + + doc_usage = "" + doc_purpose = "apply mincss to the generated site" + + logger = get_logger('mincss', STDERR_HANDLER) + + def _execute(self, options, args): + """Apply mincss the generated site.""" + output_folder = self.site.config['OUTPUT_FOLDER'] + if Processor is None: + req_missing(['mincss'], 'use the "mincss" command') + return + + p = Processor(preserve_remote_urls=False) + urls = [] + css_files = {} + for root, dirs, files in os.walk(output_folder): + for f in files: + url = os.path.join(root, f) + if url.endswith('.css'): + fname = os.path.basename(url) + if fname in css_files: + self.logger.error("You have two CSS files with the same name and that confuses me.") + sys.exit(1) + css_files[fname] = url + if not f.endswith('.html'): + continue + urls.append(url) + p.process(*urls) + for inline in p.links: + fname = os.path.basename(inline.href) + with open(css_files[fname], 'wb+') as outf: + outf.write(inline.after) diff --git a/nikola/plugins/command_new_post.plugin b/nikola/plugins/command/new_post.plugin index 6d70aff..ec35c35 100644 --- a/nikola/plugins/command_new_post.plugin +++ b/nikola/plugins/command/new_post.plugin @@ -1,10 +1,10 @@ [Core] Name = new_post -Module = command_new_post +Module = new_post [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Create a new post. diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command/new_post.py index 933a51a..ea0f3de 100644 --- a/nikola/plugins/command_new_post.py +++ b/nikola/plugins/command/new_post.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -28,20 +30,24 @@ import datetime import os import sys +from blinker import signal + from nikola.plugin_categories import Command from nikola import utils +LOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER) + -def filter_post_pages(compiler, is_post, post_compilers, post_pages): +def filter_post_pages(compiler, is_post, compilers, post_pages): """Given a compiler ("markdown", "rest"), and whether it's meant for - a post or a page, and post_compilers, return the correct entry from + a post or a page, and compilers, return the correct entry from post_pages.""" # First throw away all the post_pages with the wrong is_post filtered = [entry for entry in post_pages if entry[3] == is_post] # These are the extensions supported by the required format - extensions = post_compilers[compiler] + extensions = compilers[compiler] # Throw away the post_pages with the wrong extensions filtered = [entry for entry in filtered if any([ext in entry[0] for ext in @@ -51,13 +57,13 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages): type_name = "post" if is_post else "page" raise Exception("Can't find a way, using your configuration, to create " "a {0} in format {1}. You may want to tweak " - "post_compilers or post_pages in conf.py".format( + "COMPILERS or POSTS/PAGES in conf.py".format( type_name, compiler)) return filtered[0] -def get_default_compiler(is_post, post_compilers, post_pages): - """Given post_compilers and post_pages, return a reasonable +def get_default_compiler(is_post, compilers, post_pages): + """Given compilers and post_pages, return a reasonable default compiler for this kind of post/page. """ @@ -67,19 +73,60 @@ def get_default_compiler(is_post, post_compilers, post_pages): # Get extensions in filtered post_pages until one matches a compiler for entry in filtered: extension = os.path.splitext(entry[0])[-1] - for compiler, extensions in post_compilers.items(): + for compiler, extensions in compilers.items(): if extension in extensions: return compiler # No idea, back to default behaviour return 'rest' +def get_date(schedule=False, rule=None, last_date=None, force_today=False): + """Returns a date stamp, given a recurrence rule. + + schedule - bool: + whether to use the recurrence rule or not + + rule - str: + an iCal RRULE string that specifies the rule for scheduling posts + + last_date - datetime: + timestamp of the last post + + force_today - bool: + tries to schedule a post to today, if possible, even if the scheduled + time has already passed in the day. + """ + + date = now = datetime.datetime.now() + if schedule: + try: + from dateutil import rrule + except ImportError: + LOGGER.error('To use the --schedule switch of new_post, ' + 'you have to install the "dateutil" package.') + rrule = None + if schedule and rrule and rule: + if last_date and last_date.tzinfo: + # strip tzinfo for comparisons + last_date = last_date.replace(tzinfo=None) + try: + rule_ = rrule.rrulestr(rule, dtstart=last_date) + except Exception: + LOGGER.error('Unable to parse rule string, using current time.') + else: + # Try to post today, instead of tomorrow, if no other post today. + if force_today: + now = now.replace(hour=0, minute=0, second=0, microsecond=0) + date = rule_.after(max(now, last_date or now), last_date is None) + return date.strftime('%Y/%m/%d %H:%M:%S') + + class CommandNewPost(Command): """Create a new post.""" name = "new_post" doc_usage = "[options] [path]" - doc_purpose = "Create a new blog post or site page." + doc_purpose = "create a new blog post or site page" cmd_options = [ { 'name': 'is_page', @@ -126,12 +173,19 @@ class CommandNewPost(Command): 'default': '', 'help': 'Markup format for post, one of rest, markdown, wiki, ' 'bbcode, html, textile, txt2tags', - } + }, + { + 'name': 'schedule', + 'short': 's', + 'type': bool, + 'default': False, + 'help': 'Schedule post based on recurrence rule' + }, + ] def _execute(self, options, args): """Create a new post or page.""" - compiler_names = [p.name for p in self.site.plugin_manager.getPluginsOfCategory( "PageCompiler")] @@ -161,18 +215,18 @@ class CommandNewPost(Command): if not post_format: # Issue #400 post_format = get_default_compiler( is_post, - self.site.config['post_compilers'], + self.site.config['COMPILERS'], self.site.config['post_pages']) if post_format not in compiler_names: - print("ERROR: Unknown post format " + post_format) + LOGGER.error("Unknown post format " + post_format) return compiler_plugin = self.site.plugin_manager.getPluginByName( post_format, "PageCompiler").plugin_object # Guess where we should put this entry = filter_post_pages(post_format, is_post, - self.site.config['post_compilers'], + self.site.config['COMPILERS'], self.site.config['post_pages']) print("Creating New Post") @@ -193,7 +247,14 @@ class CommandNewPost(Command): if isinstance(path, utils.bytes_str): path = path.decode(sys.stdin.encoding) slug = utils.slugify(os.path.splitext(os.path.basename(path))[0]) - date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') + # Calculate the date to use for the post + schedule = options['schedule'] or self.site.config['SCHEDULE_ALL'] + rule = self.site.config['SCHEDULE_RULE'] + force_today = self.site.config['SCHEDULE_FORCE_TODAY'] + self.site.scan_posts() + timeline = self.site.timeline + last_date = None if not timeline else timeline[0].date + date = get_date(schedule, rule, last_date, force_today) data = [title, slug, date, tags] output_path = os.path.dirname(entry[0]) meta_path = os.path.join(output_path, slug + ".meta") @@ -206,20 +267,25 @@ class CommandNewPost(Command): if (not onefile and os.path.isfile(meta_path)) or \ os.path.isfile(txt_path): - print("The title already exists!") + LOGGER.error("The title already exists!") exit() d_name = os.path.dirname(txt_path) - if not os.path.exists(d_name): - os.makedirs(d_name) + utils.makedirs(d_name) + metadata = self.site.config['ADDITIONAL_METADATA'] compiler_plugin.create_post( txt_path, onefile, title=title, - slug=slug, date=date, tags=tags) + slug=slug, date=date, tags=tags, **metadata) + + event = dict(path=txt_path) if not onefile: # write metadata file with codecs.open(meta_path, "wb+", "utf8") as fd: fd.write('\n'.join(data)) with codecs.open(txt_path, "wb+", "utf8") as fd: fd.write("Write your post here.") - print("Your post's metadata is at: ", meta_path) - print("Your post's text is at: ", txt_path) + LOGGER.notice("Your post's metadata is at: {0}".format(meta_path)) + event['meta_path'] = meta_path + LOGGER.notice("Your post's text is at: {0}".format(txt_path)) + + signal('new_post').send(self, **event) diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command/planetoid.plugin index 8636d49..e767f31 100644 --- a/nikola/plugins/command_planetoid.plugin +++ b/nikola/plugins/command/planetoid.plugin @@ -1,9 +1,9 @@ [Core] Name = planetoid -Module = command_planetoid +Module = planetoid [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Maintain a planet-like site diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command/planetoid/__init__.py index 183dd51..369862b 100644 --- a/nikola/plugins/command_planetoid/__init__.py +++ b/nikola/plugins/command/planetoid/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2012 Roberto Alsina y otros. + +# 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 @@ -33,7 +34,9 @@ import sys from doit.tools import timeout from nikola.plugin_categories import Command, Task -from nikola.utils import config_changed +from nikola.utils import config_changed, req_missing, get_logger, STDERR_HANDLER + +LOGGER = get_logger('planetoid', STDERR_HANDLER) try: import feedparser @@ -75,10 +78,10 @@ class Planetoid(Command, Task): def gen_tasks(self): if peewee is None or sys.version_info[0] == 3: if sys.version_info[0] == 3: - message = 'Peewee is currently incompatible with Python 3.' + message = 'Peewee, a requirement of the "planetoid" command, is currently incompatible with Python 3.' else: - message = 'You need to install the \"peewee\" module.' - + req_missing('peewee', 'use the "planetoid" command') + message = '' yield { 'basename': self.name, 'name': '', @@ -159,12 +162,12 @@ class Planetoid(Command, Task): # TODO: log failure return if parsed.feed.get('title'): - print(parsed.feed.title) + LOGGER.notice(parsed.feed.title) else: - print(feed.url) + LOGGER.notice(feed.url) feed.etag = parsed.get('etag', 'foo') modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6] - print("==========>", modified) + LOGGER.notice("==========>", modified) modified = datetime.datetime(*modified) feed.last_modified = modified feed.save() @@ -173,15 +176,14 @@ class Planetoid(Command, Task): # TODO log failure return for entry_data in parsed.entries: - print("=========================================") + LOGGER.notice("=========================================") date = entry_data.get('published_parsed', None) if date is None: date = entry_data.get('updated_parsed', None) if date is None: - print("Can't parse date from:") - print(entry_data) + LOGGER.error("Can't parse date from:\n", entry_data) return False - print("DATE:===>", date) + LOGGER.notice("DATE:===>", date) date = datetime.datetime(*(date[:6])) title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título')) content = entry_data.get('content', None) @@ -193,9 +195,9 @@ class Planetoid(Command, Task): content = entry_data.get('summary', 'Sin contenido') guid = str(entry_data.get('guid', entry_data.link)) link = entry_data.link - print(repr([date, title])) + LOGGER.notice(repr([date, title])) e = list(Entry.select().where(Entry.guid == guid)) - print( + LOGGER.notice( repr(dict( date=date, title=title, diff --git a/nikola/plugins/command_serve.plugin b/nikola/plugins/command/serve.plugin index 684935d..e663cc6 100644 --- a/nikola/plugins/command_serve.plugin +++ b/nikola/plugins/command/serve.plugin @@ -1,10 +1,10 @@ [Core] Name = serve -Module = command_serve +Module = serve [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Start test server. diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py new file mode 100644 index 0000000..07403d4 --- /dev/null +++ b/nikola/plugins/command/serve.py @@ -0,0 +1,153 @@ +# -*- 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 +import os +try: + from BaseHTTPServer import HTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler +except ImportError: + from http.server import HTTPServer # NOQA + from http.server import SimpleHTTPRequestHandler # NOQA + +from nikola.plugin_categories import Command +from nikola.utils import get_logger + + +class CommandBuild(Command): + """Start test server.""" + + name = "serve" + doc_usage = "[options]" + doc_purpose = "start the test webserver" + logger = None + + cmd_options = ( + { + 'name': 'port', + 'short': 'p', + 'long': 'port', + 'default': 8000, + 'type': int, + 'help': 'Port nummber (default: 8000)', + }, + { + 'name': 'address', + 'short': 'a', + 'long': '--address', + 'type': str, + 'default': '127.0.0.1', + 'help': 'Address to bind (default: 127.0.0.1)', + }, + ) + + def _execute(self, options, args): + """Start test server.""" + self.logger = get_logger('serve', self.site.loghandlers) + out_dir = self.site.config['OUTPUT_FOLDER'] + if not os.path.isdir(out_dir): + self.logger.error("Missing '{0}' folder?".format(out_dir)) + else: + os.chdir(out_dir) + httpd = HTTPServer((options['address'], options['port']), + OurHTTPRequestHandler) + sa = httpd.socket.getsockname() + self.logger.notice("Serving HTTP on {0} port {1} ...".format(*sa)) + httpd.serve_forever() + + +class OurHTTPRequestHandler(SimpleHTTPRequestHandler): + extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) + extensions_map[""] = "text/plain" + + # NOTICE: this is a patched version of send_head() to disable all sorts of + # caching. `nikola serve` is a development server, hence caching should + # not happen to have access to the newest resources. + # + # The original code was copy-pasted from Python 2.7. Python 3.3 contains + # the same code, missing the binary mode comment. + # + # Note that it might break in future versions of Python, in which case we + # would need to do even more magic. + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + if not self.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", self.path + "/") + # begin no-cache patch + # For redirects. With redirects, caching is even worse and can + # break more. Especially with 301 Moved Permanently redirects, + # like this one. + self.send_header("Cache-Control", "no-cache, no-store, " + "must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + # end no-cache patch + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + # begin no-cache patch + # For standard requests. + self.send_header("Cache-Control", "no-cache, no-store, " + "must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + # end no-cache patch + self.end_headers() + return f diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin new file mode 100644 index 0000000..3c1ae95 --- /dev/null +++ b/nikola/plugins/command/version.plugin @@ -0,0 +1,9 @@ +[Core] +Name = version +Module = version + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Show nikola version diff --git a/nikola/plugins/compile_rest/dummy.py b/nikola/plugins/command/version.py index 39543fd..65896e9 100644 --- a/nikola/plugins/compile_rest/dummy.py +++ b/nikola/plugins/command/version.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2012 Roberto Alsina y otros. + +# 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 @@ -23,22 +24,21 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""A stupid codeblock replacement for neanderthals and users of Debian Sid.""" - -from __future__ import unicode_literals +from __future__ import print_function -from docutils import nodes -from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import Command +from nikola import __version__ -CODE = '<pre>{0}</pre>' +class CommandVersion(Command): + """Print the version.""" -class CodeBlock(Directive): - required_arguments = 1 - has_content = True + name = "version" - def run(self): - """ Required by the Directive interface. Create docutils nodes """ - return [nodes.raw('', CODE.format('\n'.join(self.content)), format='html')] + doc_usage = "" + needs_config = False + doc_purpose = "print the Nikola version number" -directives.register_directive('code', CodeBlock) + def _execute(self, options={}, args=None): + """Print the version number.""" + print("Nikola version " + __version__) diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py deleted file mode 100644 index ea82703..0000000 --- a/nikola/plugins/command_check.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import os -import sys -try: - from urllib import unquote - from urlparse import urlparse -except ImportError: - from urllib.parse import unquote, urlparse # NOQA - -import lxml.html - -from nikola.plugin_categories import Command - - -class CommandCheck(Command): - """Check the generated site.""" - - name = "check" - - doc_usage = "-l [--find-sources] | -f" - doc_purpose = "Check links and files in the generated site." - cmd_options = [ - { - 'name': 'links', - 'short': 'l', - 'long': 'check-links', - 'type': bool, - 'default': False, - 'help': 'Check for dangling links', - }, - { - 'name': 'files', - 'short': 'f', - 'long': 'check-files', - 'type': bool, - 'default': False, - 'help': 'Check for unknown files', - }, - { - 'name': 'find_sources', - 'long': 'find-sources', - 'type': bool, - 'default': False, - 'help': 'List possible source files for files with broken links.', - }, - ] - - def _execute(self, options, args): - """Check the generated site.""" - if not options['links'] and not options['files']: - print(self.help()) - return False - if options['links']: - failure = scan_links(options['find_sources']) - if options['files']: - failure = scan_files() - if failure: - sys.exit(1) - -existing_targets = set([]) - - -def analize(task, find_sources=False): - rv = False - try: - filename = task.split(":")[-1] - d = lxml.html.fromstring(open(filename).read()) - for l in d.iterlinks(): - target = l[0].attrib[l[1]] - if target == "#": - continue - parsed = urlparse(target) - if parsed.scheme: - continue - if parsed.fragment: - target = target.split('#')[0] - target_filename = os.path.abspath( - os.path.join(os.path.dirname(filename), unquote(target))) - if target_filename not in existing_targets: - if os.path.exists(target_filename): - existing_targets.add(target_filename) - else: - rv = True - print("Broken link in {0}: ".format(filename), target) - if find_sources: - print("Possible sources:") - print(os.popen('nikola list --deps ' + task, - 'r').read()) - print("===============================\n") - - except Exception as exc: - print("Error with:", filename, exc) - return rv - - -def scan_links(find_sources=False): - print("Checking Links:\n===============\n") - failure = False - for task in os.popen('nikola list --all', 'r').readlines(): - task = task.strip() - if task.split(':')[0] in ('render_tags', 'render_archive', - 'render_galleries', 'render_indexes', - 'render_pages' - 'render_site') and '.html' in task: - if analize(task, find_sources): - failure = True - return failure - - -def scan_files(): - print("Checking Files:\n===============\n") - task_fnames = set([]) - real_fnames = set([]) - # First check that all targets are generated in the right places - failure = False - for task in os.popen('nikola list --all', 'r').readlines(): - task = task.strip() - if 'output' in task and ':' in task: - fname = task.split(':')[-1] - task_fnames.add(fname) - # And now check that there are no non-target files - for root, dirs, files in os.walk('output'): - for src_name in files: - fname = os.path.join(root, src_name) - real_fnames.add(fname) - - only_on_output = list(real_fnames - task_fnames) - if only_on_output: - only_on_output.sort() - print("\nFiles from unknown origins:\n") - for f in only_on_output: - print(f) - failure = True - - only_on_input = list(task_fnames - real_fnames) - if only_on_input: - only_on_input.sort() - print("\nFiles not generated:\n") - for f in only_on_input: - print(f) - - return failure diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py deleted file mode 100644 index 3277567..0000000 --- a/nikola/plugins/command_deploy.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -from ast import literal_eval -import codecs -from datetime import datetime -import os -import subprocess - - -from nikola.plugin_categories import Command - - -class Deploy(Command): - """Deploy site. """ - name = "deploy" - - doc_usage = "" - doc_purpose = "Deploy the site." - - def _execute(self, command, args): - for command in self.site.config['DEPLOY_COMMANDS']: - - # Get last succesful deploy date - timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy') - try: - with open(timestamp_path, 'rb') as inf: - last_deploy = literal_eval(inf.read().strip()) - except Exception: - last_deploy = datetime(1970, 1, 1) # NOQA - - print("==>", command) - ret = subprocess.check_call(command, shell=True) - if ret != 0: # failed deployment - raise Exception("Failed deployment") - print("Successful deployment") - new_deploy = datetime.now() - # Store timestamp of successful deployment - with codecs.open(timestamp_path, 'wb+', 'utf8') as outf: - outf.write(repr(new_deploy)) - # Here is where we would do things with whatever is - # on self.site.timeline and is newer than - # last_deploy diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py deleted file mode 100644 index 2a0a0cc..0000000 --- a/nikola/plugins/command_install_theme.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import os -import json -from io import BytesIO - -try: - import requests -except ImportError: - requests = None # NOQA - -from nikola.plugin_categories import Command -from nikola import utils - - -class CommandInstallTheme(Command): - """Start test server.""" - - name = "install_theme" - doc_usage = "[[-u] theme_name] | [[-u] -l]" - doc_purpose = "Install theme into current site." - cmd_options = [ - { - 'name': 'list', - 'short': 'l', - 'long': 'list', - 'type': bool, - 'default': False, - 'help': 'Show list of available themes.' - }, - { - 'name': 'url', - 'short': 'u', - 'long': 'url', - 'type': str, - 'help': "URL for the theme repository (default: " - "http://nikola.ralsina.com.ar/themes/index.json)", - 'default': 'http://nikola.ralsina.com.ar/themes/index.json' - }, - ] - - def _execute(self, options, args): - """Install theme into current site.""" - if requests is None: - print('This command requires the requests package be installed.') - return False - - listing = options['list'] - url = options['url'] - if args: - name = args[0] - else: - name = None - - if name is None and not listing: - print("This command needs either a theme name or the -l option.") - return False - data = requests.get(url).text - data = json.loads(data) - if listing: - print("Themes:") - print("-------") - for theme in sorted(data.keys()): - print(theme) - return True - else: - if name in data: - if os.path.isfile("themes"): - raise IOError("'themes' isn't a directory!") - elif not os.path.isdir("themes"): - try: - os.makedirs("themes") - except: - raise OSError("mkdir 'theme' error!") - print('Downloading: ' + data[name]) - zip_file = BytesIO() - zip_file.write(requests.get(data[name]).content) - print('Extracting: {0} into themes'.format(name)) - utils.extract_all(zip_file) - else: - print("Can't find theme " + name) - return False diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py deleted file mode 100644 index 64efe7d..0000000 --- a/nikola/plugins/command_serve.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import os -try: - from BaseHTTPServer import HTTPServer - from SimpleHTTPServer import SimpleHTTPRequestHandler -except ImportError: - from http.server import HTTPServer # NOQA - from http.server import SimpleHTTPRequestHandler # NOQA - -from nikola.plugin_categories import Command - - -class CommandBuild(Command): - """Start test server.""" - - name = "serve" - doc_usage = "[options]" - doc_purpose = "Start the test webserver." - - cmd_options = ( - { - 'name': 'port', - 'short': 'p', - 'long': 'port', - 'default': 8000, - 'type': int, - 'help': 'Port nummber (default: 8000)', - }, - { - 'name': 'address', - 'short': 'a', - 'long': '--address', - 'type': str, - 'default': '127.0.0.1', - 'help': 'Address to bind (default: 127.0.0.1)', - }, - ) - - def _execute(self, options, args): - """Start test server.""" - out_dir = self.site.config['OUTPUT_FOLDER'] - if not os.path.isdir(out_dir): - print("Error: Missing '{0}' folder?".format(out_dir)) - else: - os.chdir(out_dir) - httpd = HTTPServer((options['address'], options['port']), - OurHTTPRequestHandler) - sa = httpd.socket.getsockname() - print("Serving HTTP on", sa[0], "port", sa[1], "...") - httpd.serve_forever() - - -class OurHTTPRequestHandler(SimpleHTTPRequestHandler): - extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) - extensions_map[""] = "text/plain" diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/plugins/compile/__init__.py diff --git a/nikola/plugins/compile/asciidoc.plugin b/nikola/plugins/compile/asciidoc.plugin new file mode 100644 index 0000000..47c5608 --- /dev/null +++ b/nikola/plugins/compile/asciidoc.plugin @@ -0,0 +1,10 @@ +[Core] +Name = asciidoc +Module = asciidoc + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile ASCIIDoc into HTML + diff --git a/nikola/plugins/compile/asciidoc.py b/nikola/plugins/compile/asciidoc.py new file mode 100644 index 0000000..67dfe1a --- /dev/null +++ b/nikola/plugins/compile/asciidoc.py @@ -0,0 +1,65 @@ +# -*- 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. + +"""Implementation of compile_html based on asciidoc. + +You will need, of course, to install asciidoc + +""" + +import codecs +import os +import subprocess + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing + + +class CompileAsciiDoc(PageCompiler): + """Compile asciidoc into HTML.""" + + name = "asciidoc" + + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) + try: + subprocess.check_call(('asciidoc', '-f', 'html', '-s', '-o', dest, source)) + except OSError as e: + if e.strreror == 'No such file or directory': + req_missing(['asciidoc'], 'build this site (compile with asciidoc)', python=False) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write("/////////////////////////////////////////////\n") + for k, v in metadata.items(): + fd.write('.. {0}: {1}\n'.format(k, v)) + fd.write("/////////////////////////////////////////////\n") + fd.write("\nWrite your post here.") diff --git a/nikola/plugins/compile_bbcode.plugin b/nikola/plugins/compile/bbcode.plugin index ec3ce2b..b3d9357 100644 --- a/nikola/plugins/compile_bbcode.plugin +++ b/nikola/plugins/compile/bbcode.plugin @@ -1,10 +1,10 @@ [Core] Name = bbcode -Module = compile_bbcode +Module = bbcode [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile BBCode into HTML diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile/bbcode.py index f8022f3..e998417 100644 --- a/nikola/plugins/compile_bbcode.py +++ b/nikola/plugins/compile/bbcode.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,6 +28,7 @@ import codecs import os +import re try: import bbcode @@ -33,9 +36,10 @@ except ImportError: bbcode = None # NOQA from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing -class CompileTextile(PageCompiler): +class CompileBbcode(PageCompiler): """Compile bbcode into HTML.""" name = "bbcode" @@ -46,17 +50,15 @@ class CompileTextile(PageCompiler): self.parser = bbcode.Parser() self.parser.add_simple_formatter("note", "") - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if bbcode is None: - raise Exception('To build this site, you need to install the ' - '"bbcode" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['bbcode'], 'build this site (compile BBCode)') + makedirs(os.path.dirname(dest)) with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] output = self.parser.format(data) out_file.write(output) @@ -64,9 +66,7 @@ class CompileTextile(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('[note]<!--\n') diff --git a/nikola/plugins/compile_html.plugin b/nikola/plugins/compile/html.plugin index f6cdfbc..21dd338 100644 --- a/nikola/plugins/compile_html.plugin +++ b/nikola/plugins/compile/html.plugin @@ -1,10 +1,10 @@ [Core] Name = html -Module = compile_html +Module = html [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile HTML into HTML (just copy) diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile/html.py index 7551b33..a309960 100644 --- a/nikola/plugins/compile_html.py +++ b/nikola/plugins/compile/html.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -22,13 +24,14 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on markdown.""" +"""Implementation of compile_html for HTML source files.""" import os import shutil import codecs from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs class CompileHtml(PageCompiler): @@ -36,24 +39,20 @@ class CompileHtml(PageCompiler): name = "html" - def compile_html(self, source, dest): - try: - os.makedirs(os.path.dirname(dest)) - except Exception: - pass + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) shutil.copyfile(source, dest) + return True def create_post(self, path, onefile=False, **kw): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<!-- \n') - for k, v in metadata.keys(): + for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') fd.write("\n<p>Write your post here.</p>") diff --git a/nikola/plugins/compile_ipynb.plugin b/nikola/plugins/compile/ipynb.plugin index 51051e0..3d15bb0 100644 --- a/nikola/plugins/compile_ipynb.plugin +++ b/nikola/plugins/compile/ipynb.plugin @@ -1,10 +1,10 @@ [Core] Name = ipynb -Module = compile_ipynb +Module = ipynb [Documentation] Author = Damián Avila -Version = 0.1 +Version = 1.0 Website = http://www.oquanta.info Description = Compile IPython notebooks into HTML diff --git a/nikola/plugins/compile/ipynb/README.txt b/nikola/plugins/compile/ipynb/README.txt new file mode 100644 index 0000000..0a7d6db --- /dev/null +++ b/nikola/plugins/compile/ipynb/README.txt @@ -0,0 +1,44 @@ +To make this work... + +1- You can install the "jinja-site-ipython" theme using this command: + +$ nikola install_theme -n jinja-site-ipython + +(or xkcd-site-ipython, if you want xkcd styling) + +More info here about themes: +http://getnikola.com/handbook.html#getting-more-themes + +OR + +You can to download the "jinja-site-ipython" theme from here: +https://github.com/damianavila/jinja-site-ipython-theme-for-Nikola +and copy the "site-ipython" folder inside the "themes" folder of your site. + + +2- Then, just add: + +post_pages = ( + ("posts/*.ipynb", "posts", "post.tmpl", True), + ("stories/*.ipynb", "stories", "story.tmpl", False), +) + +and + +THEME = 'jinja-site-ipython' (or 'xkcd-site-ipython', if you want xkcd styling) + +to your conf.py. +Finally... to use it: + +$nikola new_page -f ipynb + +**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler +create one metadata file and the corresponding naive IPython notebook. + +$nikola build + +And deploy the output folder... to see it locally: $nikola serve +If you have any doubts, just ask: @damianavila + +Cheers. +Damián diff --git a/nikola/plugins/compile_ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py index d38f6f2..7c318ca 100644 --- a/nikola/plugins/compile_ipynb/__init__.py +++ b/nikola/plugins/compile/ipynb/__init__.py @@ -1,4 +1,6 @@ -# Copyright (c) 2013 Damian Avila. +# -*- coding: utf-8 -*- + +# Copyright © 2013 Damian Avila. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,13 +31,15 @@ import codecs import os try: - from .nbformat import current as nbformat - from .nbconvert.converters import bloggerhtml as nbconverter - bloggerhtml = True + from IPython.nbconvert.exporters import HTMLExporter + from IPython.nbformat import current as nbformat + from IPython.config import Config + flag = True except ImportError: - bloggerhtml = None + flag = None from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing class CompileIPynb(PageCompiler): @@ -43,30 +47,26 @@ class CompileIPynb(PageCompiler): name = "ipynb" - def compile_html(self, source, dest): - if bloggerhtml is None: - raise Exception('To build this site, you also need ' - 'https://github.com/damianavila/com' - 'pile_ipynb-for-Nikola.git.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass - converter = nbconverter.ConverterBloggerHTML() + def compile_html(self, source, dest, is_two_file=True): + if flag is None: + req_missing(['ipython>=1.0.0'], 'build this site (compile ipynb)') + makedirs(os.path.dirname(dest)) + HTMLExporter.default_template = 'basic' + c = Config(self.site.config['IPYNB_CONFIG']) + exportHtml = HTMLExporter(config=c) with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: - data = in_file.read() - converter.nb = nbformat.reads_json(data) - output = converter.convert() - out_file.write(output) + nb = in_file.read() + nb_json = nbformat.reads_json(nb) + (body, resources) = exportHtml.from_notebook_node(nb_json) + out_file.write(body) def create_post(self, path, onefile=False, **kw): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) meta_path = os.path.join(d_name, kw['slug'] + ".meta") with codecs.open(meta_path, "wb+", "utf8") as fd: if onefile: @@ -78,7 +78,7 @@ class CompileIPynb(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: fd.write("""{ "metadata": { - "name": "%s" + "name": "" }, "nbformat": 3, "nbformat_minor": 0, @@ -97,4 +97,4 @@ class CompileIPynb(PageCompiler): "metadata": {} } ] -}""" % kw['slug']) +}""") diff --git a/nikola/plugins/compile_markdown.plugin b/nikola/plugins/compile/markdown.plugin index f3e119b..157579a 100644 --- a/nikola/plugins/compile_markdown.plugin +++ b/nikola/plugins/compile/markdown.plugin @@ -1,10 +1,10 @@ [Core] Name = markdown -Module = compile_markdown +Module = markdown [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile Markdown into HTML diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index ae700e6..b41c6b5 100644 --- a/nikola/plugins/compile_markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -28,17 +30,18 @@ from __future__ import unicode_literals import codecs import os +import re try: from markdown import markdown - from nikola.plugins.compile_markdown.mdx_nikola import NikolaExtension + from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension nikola_extension = NikolaExtension() - from nikola.plugins.compile_markdown.mdx_gist import GistExtension + from nikola.plugins.compile.markdown.mdx_gist import GistExtension gist_extension = GistExtension() - from nikola.plugins.compile_markdown.mdx_podcast import PodcastExtension + from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension podcast_extension = PodcastExtension() except ImportError: @@ -48,27 +51,26 @@ except ImportError: podcast_extension = None from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing class CompileMarkdown(PageCompiler): """Compile markdown into HTML.""" name = "markdown" + extensions = [gist_extension, nikola_extension, podcast_extension] + site = None - extensions = ['fenced_code', 'codehilite', gist_extension, - nikola_extension, podcast_extension] - - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if markdown is None: - raise Exception('To build this site, you need to install the ' - '"markdown" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['markdown'], 'build this site (compile Markdown)') + makedirs(os.path.dirname(dest)) + self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS") with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] output = markdown(data, self.extensions) out_file.write(output) @@ -76,9 +78,7 @@ class CompileMarkdown(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<!-- \n') diff --git a/nikola/plugins/compile_markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index 808e383..3c3bef9 100644 --- a/nikola/plugins/compile_markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -21,12 +21,11 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # +# Warning: URL formats of "raw" gists are undocummented and subject to change. +# See also: http://developer.github.com/v3/gists/ +# # Inspired by "[Python] reStructuredText GitHub Gist directive" # (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu - -from __future__ import print_function - - ''' Extension to Python Markdown for Embedded Gists (gist.github.com) @@ -83,13 +82,48 @@ Example using reStructuredText syntax: </noscript> </div> </p> + +Error Case: non-existent Gist ID: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 0] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) + <p>Text of the gist: + <div class="gist"> + <script src="https://gist.github.com/0.js"></script> + <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/0 --></noscript> + </div> + </p> + +Error Case: non-existent file: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 4747847 doesntexist.py] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) + <p>Text of the gist: + <div class="gist"> + <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script> + <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/4747847/doesntexist.py --></noscript> + </div> + </p> + ''' -from __future__ import unicode_literals -import warnings +from __future__ import unicode_literals, print_function from markdown.extensions import Extension from markdown.inlinepatterns import Pattern from markdown.util import AtomicString from markdown.util import etree +from nikola.utils import get_logger, req_missing, STDERR_HANDLER + +LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER) try: import requests @@ -98,13 +132,21 @@ except ImportError: GIST_JS_URL = "https://gist.github.com/{0}.js" GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}" -GIST_RAW_URL = "https://raw.github.com/gist/{0}" -GIST_FILE_RAW_URL = "https://raw.github.com/gist/{0}/{1}" +GIST_RAW_URL = "https://gist.github.com/raw/{0}" +GIST_FILE_RAW_URL = "https://gist.github.com/raw/{0}/{1}" -GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\]' +GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\s*\]' GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$' +class GistFetchException(Exception): + '''Raised when attempt to fetch content of a Gist from github.com fails.''' + def __init__(self, url, status_code): + Exception.__init__(self) + self.message = 'Received a {0} response from Gist URL: {1}'.format( + status_code, url) + + class GistPattern(Pattern): """ InlinePattern for footnote markers in a document's body text. """ @@ -113,11 +155,21 @@ class GistPattern(Pattern): def get_raw_gist_with_filename(self, gist_id, filename): url = GIST_FILE_RAW_URL.format(gist_id, filename) - return requests.get(url).text + resp = requests.get(url) + + if not resp.ok: + raise GistFetchException(url, resp.status_code) + + return resp.text def get_raw_gist(self, gist_id): url = GIST_RAW_URL.format(gist_id) - return requests.get(url).text + resp = requests.get(url) + + if not resp.ok: + raise GistFetchException(url, resp.status_code) + + return resp.text def handleMatch(self, m): gist_id = m.group('gist_id') @@ -127,34 +179,32 @@ class GistPattern(Pattern): gist_elem.set('class', 'gist') script_elem = etree.SubElement(gist_elem, 'script') - if gist_file: - script_elem.set('src', GIST_FILE_JS_URL.format( - gist_id, gist_file)) - - else: - script_elem.set('src', GIST_JS_URL.format( - gist_id)) - if requests: - if gist_file: - raw_gist = (self.get_raw_gist_with_filename( - gist_id, gist_file)) - script_elem.set('src', GIST_FILE_JS_URL.format( - gist_id, gist_file)) - - else: - raw_gist = (self.get_raw_gist(gist_id)) - script_elem.set('src', GIST_JS_URL.format( - gist_id)) - - # Insert source as <pre/> within <noscript> noscript_elem = etree.SubElement(gist_elem, 'noscript') - pre_elem = etree.SubElement(noscript_elem, 'pre') - pre_elem.text = AtomicString(raw_gist) + + try: + if gist_file: + script_elem.set('src', GIST_FILE_JS_URL.format( + gist_id, gist_file)) + raw_gist = (self.get_raw_gist_with_filename( + gist_id, gist_file)) + + else: + script_elem.set('src', GIST_JS_URL.format( + gist_id)) + raw_gist = (self.get_raw_gist(gist_id)) + + # Insert source as <pre/> within <noscript> + pre_elem = etree.SubElement(noscript_elem, 'pre') + pre_elem.text = AtomicString(raw_gist) + + except GistFetchException as e: + LOGGER.warn(e.message) + warning_comment = etree.Comment(' WARNING: {0} '.format(e.message)) + noscript_elem.append(warning_comment) else: - warnings.warn('"requests" package not installed. ' - 'Please install to add inline gist source.') + req_missing('requests', 'have inline gist source', optional=True) return gist_elem @@ -185,5 +235,7 @@ def makeExtension(configs=None): if __name__ == '__main__': import doctest + + # Silence user warnings thrown by tests: doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE + doctest.REPORT_NDIFF)) diff --git a/nikola/plugins/compile_markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py index f7a1959..b0ad2f7 100644 --- a/nikola/plugins/compile_markdown/mdx_nikola.py +++ b/nikola/plugins/compile/markdown/mdx_nikola.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 diff --git a/nikola/plugins/compile_markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py index be8bb6b..be8bb6b 100644 --- a/nikola/plugins/compile_markdown/mdx_podcast.py +++ b/nikola/plugins/compile/markdown/mdx_podcast.py diff --git a/nikola/plugins/compile_misaka.plugin b/nikola/plugins/compile/misaka.plugin index 1b9c8a8..fef6d71 100644 --- a/nikola/plugins/compile_misaka.plugin +++ b/nikola/plugins/compile/misaka.plugin @@ -1,6 +1,6 @@ [Core] Name = misaka -Module = compile_misaka +Module = misaka [Documentation] Author = Chris Lee diff --git a/nikola/plugins/compile_misaka/__init__.py b/nikola/plugins/compile/misaka.py index a3f687e..3733a85 100644 --- a/nikola/plugins/compile_misaka/__init__.py +++ b/nikola/plugins/compile/misaka.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # Copyright (c) 2013 Chris Lee # Permission is hereby granted, free of charge, to any @@ -28,10 +30,10 @@ from __future__ import unicode_literals import codecs import os +import re try: import misaka - except ImportError: misaka = None # NOQA nikola_extension = None @@ -39,30 +41,29 @@ except ImportError: podcast_extension = None from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing -class CompileMarkdown(PageCompiler): - """Compile markdown into HTML.""" +class CompileMisaka(PageCompiler): + """Compile Misaka into HTML.""" - name = "markdown" + name = "misaka" def __init__(self, *args, **kwargs): - super(CompileMarkdown, self).__init__(*args, **kwargs) + super(CompileMisaka, self).__init__(*args, **kwargs) if misaka is not None: self.ext = misaka.EXT_FENCED_CODE | misaka.EXT_STRIKETHROUGH | \ misaka.EXT_AUTOLINK | misaka.EXT_NO_INTRA_EMPHASIS - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if misaka is None: - raise Exception('To build this site, you need to install the ' - '"misaka" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['misaka'], 'build this site (compile with misaka)') + makedirs(os.path.dirname(dest)) with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] output = misaka.html(data, extensions=self.ext) out_file.write(output) @@ -70,9 +71,7 @@ class CompileMarkdown(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<!-- \n') diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin new file mode 100644 index 0000000..157b694 --- /dev/null +++ b/nikola/plugins/compile/pandoc.plugin @@ -0,0 +1,10 @@ +[Core] +Name = pandoc +Module = pandoc + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile markups into HTML using pandoc + diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py new file mode 100644 index 0000000..3a2911f --- /dev/null +++ b/nikola/plugins/compile/pandoc.py @@ -0,0 +1,65 @@ +# -*- 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. + +"""Implementation of compile_html based on pandoc. + +You will need, of course, to install pandoc + +""" + +import codecs +import os +import subprocess + +from nikola.plugin_categories import PageCompiler +from nikola.utils import req_missing, makedirs + + +class CompilePandoc(PageCompiler): + """Compile markups into HTML using pandoc.""" + + name = "pandoc" + + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) + try: + subprocess.check_call(('pandoc', '-o', dest, source)) + except OSError as e: + if e.strreror == 'No such file or directory': + req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write('<!-- \n') + for k, v in metadata.items(): + fd.write('.. {0}: {1}\n'.format(k, v)) + fd.write('-->\n\n') + fd.write("Write your post here.") diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin new file mode 100644 index 0000000..ac25259 --- /dev/null +++ b/nikola/plugins/compile/php.plugin @@ -0,0 +1,10 @@ +[Core] +Name = php +Module = php + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile PHP into HTML (just copy and name the file .php) + diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py new file mode 100644 index 0000000..44701c8 --- /dev/null +++ b/nikola/plugins/compile/php.py @@ -0,0 +1,62 @@ +# -*- 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. + +"""Implementation of compile_html for HTML+php.""" + +from __future__ import unicode_literals + +import os +import shutil +import codecs + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs + + +class CompilePhp(PageCompiler): + """Compile PHP into PHP.""" + + name = "php" + + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) + shutil.copyfile(source, dest) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + os.makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write('<!-- \n') + for k, v in metadata.items(): + fd.write('.. {0}: {1}\n'.format(k, v)) + fd.write('-->\n\n') + fd.write("\n<p>Write your post here.</p>") + + def extension(self): + return ".php" diff --git a/nikola/plugins/compile_rest.plugin b/nikola/plugins/compile/rest.plugin index 67eb562..55e9c59 100644 --- a/nikola/plugins/compile_rest.plugin +++ b/nikola/plugins/compile/rest.plugin @@ -1,10 +1,10 @@ [Core] Name = rest -Module = compile_rest +Module = rest [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile reSt into HTML diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py new file mode 100644 index 0000000..c71a5f8 --- /dev/null +++ b/nikola/plugins/compile/rest/__init__.py @@ -0,0 +1,200 @@ +# -*- 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 os +import re + +try: + import docutils.core + import docutils.nodes + import docutils.utils + import docutils.io + import docutils.readers.standalone + has_docutils = True +except ImportError: + has_docutils = False + +from nikola.plugin_categories import PageCompiler +from nikola.utils import get_logger, makedirs, req_missing + + +class CompileRest(PageCompiler): + """Compile reSt into HTML.""" + + name = "rest" + logger = None + + def compile_html(self, source, dest, is_two_file=True): + """Compile reSt into HTML.""" + + if not has_docutils: + req_missing(['docutils'], 'build this site (compile reStructuredText)') + makedirs(os.path.dirname(dest)) + error_level = 100 + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + data = in_file.read() + add_ln = 0 + if not is_two_file: + spl = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1) + data = spl[-1] + if len(spl) != 1: + # If errors occur, this will be added to the line + # number reported by docutils so the line number + # matches the actual line number (off by 7 with default + # metadata, could be more or less depending on the post + # author). + add_ln = len(spl[0].splitlines()) + 1 + + output, error_level, deps = rst2html( + data, settings_overrides={ + 'initial_header_level': 2, + 'record_dependencies': True, + 'stylesheet_path': None, + 'link_stylesheet': True, + 'syntax_highlight': 'short', + 'math_output': 'mathjax', + }, logger=self.logger, l_source=source, l_add_ln=add_ln) + out_file.write(output) + deps_path = dest + '.dep' + if deps.list: + with codecs.open(deps_path, "wb+", "utf8") as deps_file: + deps_file.write('\n'.join(deps.list)) + else: + if os.path.isfile(deps_path): + os.unlink(deps_path) + if error_level < 3: + return True + else: + return False + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + for k, v in metadata.items(): + fd.write('.. {0}: {1}\n'.format(k, v)) + fd.write("\nWrite your post here.") + + def set_site(self, site): + for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"): + if (plugin_info.name in site.config['DISABLED_PLUGINS'] + or (plugin_info.name in site.EXTRA_PLUGINS and + plugin_info.name not in site.config['ENABLED_EXTRAS'])): + site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension") + continue + + site.plugin_manager.activatePluginByName(plugin_info.name) + plugin_info.plugin_object.set_site(site) + plugin_info.plugin_object.short_help = plugin_info.description + + self.logger = get_logger('compile_rest', site.loghandlers) + return super(CompileRest, self).set_site(site) + + +def get_observer(settings): + """Return an observer for the docutils Reporter.""" + def observer(msg): + """Report docutils/rest messages to a Nikola user. + + Error code mapping: + + +------+---------+------+----------+ + | dNUM | dNAME | lNUM | lNAME | d = docutils, l = logbook + +------+---------+------+----------+ + | 0 | DEBUG | 1 | DEBUG | + | 1 | INFO | 2 | INFO | + | 2 | WARNING | 4 | WARNING | + | 3 | ERROR | 5 | ERROR | + | 4 | SEVERE | 6 | CRITICAL | + +------+---------+------+----------+ + """ + errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6} + text = docutils.nodes.Element.astext(msg) + out = '[{source}:{line}] {text}'.format(source=settings['source'], line=msg['line'] + settings['add_ln'], text=text) + settings['logger'].log(errormap[msg['level']], out) + + return observer + + +class NikolaReader(docutils.readers.standalone.Reader): + + def new_document(self): + """Create and return a new empty document tree (root node).""" + document = docutils.utils.new_document(self.source.source_path, self.settings) + document.reporter.stream = False + document.reporter.attach_observer(get_observer(self.l_settings)) + return document + + +def rst2html(source, source_path=None, source_class=docutils.io.StringInput, + destination_path=None, reader=None, + parser=None, parser_name='restructuredtext', writer=None, + writer_name='html', settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=None, logger=None, l_source='', l_add_ln=0): + """ + Set up & run a `Publisher`, and return a dictionary of document parts. + Dictionary keys are the names of parts, and values are Unicode strings; + encoding is up to the client. For programmatic use with string I/O. + + For encoded string input, be sure to set the 'input_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + input. Here's how:: + + publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) + + Parameters: see `publish_programmatically`. + + WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report + reStructuredText syntax errors. + """ + if reader is None: + reader = NikolaReader() + # For our custom logging, we have special needs and special settings we + # specify here. + # logger a logger from Nikola + # source source filename (docutils gets a string) + # add_ln amount of metadata lines (see comment in compile_html above) + reader.l_settings = {'logger': logger, 'source': l_source, + 'add_ln': l_add_ln} + + pub = docutils.core.Publisher(reader, parser, writer, settings=settings, + source_class=source_class, + destination_class=docutils.io.StringOutput) + pub.set_components(None, parser_name, writer_name) + pub.process_programmatic_settings( + settings_spec, settings_overrides, config_section) + pub.set_source(source, source_path) + pub.set_destination(None, destination_path) + pub.publish(enable_exit_status=enable_exit_status) + + return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin new file mode 100644 index 0000000..3e27a25 --- /dev/null +++ b/nikola/plugins/compile/rest/chart.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_chart +Module = chart + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Chart directive based in PyGal + diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py new file mode 100644 index 0000000..ee917b9 --- /dev/null +++ b/nikola/plugins/compile/rest/chart.py @@ -0,0 +1,150 @@ +# -*- 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 ast import literal_eval + +from docutils import nodes +from docutils.parsers.rst import Directive, directives + +try: + import pygal +except ImportError: + pygal = None # NOQA + +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + + +class Plugin(RestExtension): + + name = "rest_chart" + + def set_site(self, site): + self.site = site + directives.register_directive('chart', Chart) + return super(Plugin, self).set_site(site) + + +class Chart(Directive): + """ Restructured text extension for inserting charts as SVG + + Usage: + .. chart:: Bar + :title: 'Browser usage evolution (in %)' + :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"] + + 'Firefox', [None, None, 0, 16.6, 25, 31] + 'Chrome', [None, None, None, None, None, None] + 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6] + 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4] + """ + + has_content = True + required_arguments = 1 + option_spec = { + "copy": directives.unchanged, + "css": directives.unchanged, + "disable_xml_declaration": directives.unchanged, + "dots_size": directives.unchanged, + "explicit_size": directives.unchanged, + "fill": directives.unchanged, + "font_sizes": directives.unchanged, + "height": directives.unchanged, + "human_readable": directives.unchanged, + "include_x_axis": directives.unchanged, + "interpolate": directives.unchanged, + "interpolation_parameters": directives.unchanged, + "interpolation_precision": directives.unchanged, + "js": directives.unchanged, + "label_font_size": directives.unchanged, + "legend_at_bottom": directives.unchanged, + "legend_box_size": directives.unchanged, + "legend_font_size": directives.unchanged, + "logarithmic": directives.unchanged, + "major_label_font_size": directives.unchanged, + "margin": directives.unchanged, + "no_data_font_size": directives.unchanged, + "no_data_text": directives.unchanged, + "no_prefix": directives.unchanged, + "order_min": directives.unchanged, + "pretty_print": directives.unchanged, + "print_values": directives.unchanged, + "print_zeroes": directives.unchanged, + "range": directives.unchanged, + "rounded_bars": directives.unchanged, + "show_dots": directives.unchanged, + "show_legend": directives.unchanged, + "show_minor_x_labels": directives.unchanged, + "show_y_labels": directives.unchanged, + "spacing": directives.unchanged, + "strict": directives.unchanged, + "stroke": directives.unchanged, + "style": directives.unchanged, + "title": directives.unchanged, + "title_font_size": directives.unchanged, + "to_dict": directives.unchanged, + "tooltip_border_radius": directives.unchanged, + "tooltip_font_size": directives.unchanged, + "truncate_label": directives.unchanged, + "truncate_legend": directives.unchanged, + "value_font_size": directives.unchanged, + "value_formatter": directives.unchanged, + "width": directives.unchanged, + "x_label_rotation": directives.unchanged, + "x_labels": directives.unchanged, + "x_labels_major": directives.unchanged, + "x_labels_major_count": directives.unchanged, + "x_labels_major_every": directives.unchanged, + "x_title": directives.unchanged, + "y_label_rotation": directives.unchanged, + "y_labels": directives.unchanged, + "y_title": directives.unchanged, + "zero": directives.unchanged, + } + + def run(self): + if pygal is None: + msg = req_missing(['pygal'], 'use the Chart directive', optional=True) + return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] + options = {} + if 'style' in self.options: + style_name = self.options.pop('style') + else: + style_name = 'BlueStyle' + if '(' in style_name: # Parametric style + style = eval('pygal.style.' + style_name) + else: + style = getattr(pygal.style, style_name) + for k, v in self.options.items(): + options[k] = literal_eval(v) + + chart = getattr(pygal, self.arguments[0])(style=style) + chart.config(**options) + for line in self.content: + label, series = literal_eval('({0})'.format(line)) + chart.add(label, series) + + return [nodes.raw('', chart.render().decode('utf8'), format='html')] diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin new file mode 100644 index 0000000..1984f52 --- /dev/null +++ b/nikola/plugins/compile/rest/doc.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_doc +Module = doc + +[Documentation] +Author = Manuel Kaufmann +Version = 0.1 +Website = http://getnikola.com +Description = Role to link another page / post from the blog + diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py new file mode 100644 index 0000000..915a7e1 --- /dev/null +++ b/nikola/plugins/compile/rest/doc.py @@ -0,0 +1,88 @@ +# -*- 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 docutils import nodes +from docutils.parsers.rst import roles + +from nikola.utils import split_explicit_title +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + + name = 'rest_doc' + + def set_site(self, site): + self.site = site + roles.register_canonical_role('doc', doc_role) + doc_role.site = site + return super(Plugin, self).set_site(site) + + +def doc_role(name, rawtext, text, lineno, inliner, + options={}, content=[]): + + # split link's text and post's slug in role content + has_explicit_title, title, slug = split_explicit_title(text) + + # check if the slug given is part of our blog posts/pages + twin_slugs = False + post = None + for p in doc_role.site.timeline: + if p.meta('slug') == slug: + if post is None: + post = p + else: + twin_slugs = True + break + + try: + if post is None: + raise ValueError + except ValueError: + msg = inliner.reporter.error( + '"{0}" slug doesn\'t exist.'.format(slug), + line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + + if not has_explicit_title: + # use post's title as link's text + title = post.title() + + permalink = post.permalink() + if twin_slugs: + msg = inliner.reporter.warning( + 'More than one post with the same slug. Using "{0}"'.format(permalink)) + + node = make_link_node(rawtext, title, permalink, options) + return [node], [] + + +def make_link_node(rawtext, text, url, options): + node = nodes.reference(rawtext, text, refuri=url, *options) + return node diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin new file mode 100644 index 0000000..8f498ec --- /dev/null +++ b/nikola/plugins/compile/rest/gist.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_gist +Module = gist + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Gist directive + diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile/rest/gist.py index 1506519..e09ed76 100644 --- a/nikola/plugins/compile_rest/gist_directive.py +++ b/nikola/plugins/compile/rest/gist.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # This file is public domain according to its author, Brian Hsu from docutils.parsers.rst import Directive, directives @@ -8,13 +9,32 @@ try: except ImportError: requests = None # NOQA +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + + +class Plugin(RestExtension): + + name = "rest_gist" + + def set_site(self, site): + self.site = site + directives.register_directive('gist', GitHubGist) + return super(Plugin, self).set_site(site) + class GitHubGist(Directive): """ Embed GitHub Gist. Usage: + .. gist:: GIST_ID + or + + .. gist:: GIST_URL + + """ required_arguments = 1 @@ -24,33 +44,41 @@ class GitHubGist(Directive): has_content = False def get_raw_gist_with_filename(self, gistID, filename): - url = '/'.join(("https://raw.github.com/gist", gistID, filename)) + url = '/'.join(("https://gist.github.com/raw", gistID, filename)) return requests.get(url).text def get_raw_gist(self, gistID): - url = "https://raw.github.com/gist/{0}".format(gistID) + url = "https://gist.github.com/raw/{0}".format(gistID) return requests.get(url).text def run(self): - if requests is None: - print('To use the gist directive, you need to install the ' - '"requests" package.') - return [] - gistID = self.arguments[0].strip() + if 'https://' in self.arguments[0]: + gistID = self.arguments[0].split('/')[-1].strip() + else: + gistID = self.arguments[0].strip() embedHTML = "" rawGist = "" if 'file' in self.options: filename = self.options['file'] - rawGist = (self.get_raw_gist_with_filename(gistID, filename)) + if requests is not None: + rawGist = (self.get_raw_gist_with_filename(gistID, filename)) embedHTML = ('<script src="https://gist.github.com/{0}.js' '?file={1}"></script>').format(gistID, filename) else: - rawGist = (self.get_raw_gist(gistID)) + if requests is not None: + rawGist = (self.get_raw_gist(gistID)) embedHTML = ('<script src="https://gist.github.com/{0}.js">' '</script>').format(gistID) + if requests is None: + reqnode = nodes.raw( + '', req_missing('requests', 'have inline gist source', + optional=True), format='html') + else: + reqnode = nodes.literal_block('', rawGist) + return [nodes.raw('', embedHTML, format='html'), nodes.raw('', '<noscript>', format='html'), - nodes.literal_block('', rawGist), + reqnode, nodes.raw('', '</noscript>', format='html')] diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin new file mode 100644 index 0000000..4c9883e --- /dev/null +++ b/nikola/plugins/compile/rest/listing.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_listing +Module = listing + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Extension for source listings + diff --git a/nikola/plugins/compile_rest/listing.py b/nikola/plugins/compile/rest/listing.py index 1b816f5..31975bb 100644 --- a/nikola/plugins/compile_rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -1,121 +1,109 @@ -# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# 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.
-
-
-""" Define and register a listing directive using the existing CodeBlock """
-
-
-from __future__ import unicode_literals
-from codecs import open as codecs_open # for patching purposes
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
-
-from docutils import core
-from docutils.parsers.rst import directives
-try:
- from docutils.parsers.rst.directives.body import CodeBlock
-except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
- from dummy import CodeBlock # NOQA
-
-import os
-
-
-class Listing(CodeBlock):
- """ listing directive: create a CodeBlock from file
-
- Usage:
-
- .. listing:: nikola.py python
- :number-lines:
-
- """
- has_content = False
- required_arguments = 1
- optional_arguments = 1
-
- option_spec = {
- 'start-at': directives.unchanged,
- 'end-at': directives.unchanged,
- 'start-after': directives.unchanged,
- 'end-before': directives.unchanged,
- }
-
- def run(self):
- fname = self.arguments.pop(0)
- with codecs_open(os.path.join('listings', fname), 'rb+', 'utf8') as fileobject:
- self.content = fileobject.read().splitlines()
- self.trim_content()
- target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = (
- [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
- generated_nodes += self.get_code_from_file(fileobject)
- return generated_nodes
-
- def trim_content(self):
- """Cut the contents based in options."""
- start = 0
- end = len(self.content)
- if 'start-at' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-at'] in l:
- break
- else:
- start = 0
- elif 'start-before' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-before'] in l:
- if start > 0:
- start -= 1
- break
- else:
- start = 0
- if 'end-at' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-at'] in l:
- break
- else:
- end = len(self.content)
- elif 'end-before' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-before'] in l:
- end -= 1
- break
- else:
- end = len(self.content)
-
- self.content = self.content[start:end]
-
- def get_code_from_file(self, data):
- """ Create CodeBlock nodes from file object content """
- return super(Listing, self).run()
-
- def assert_has_content(self):
- """ Listing has no content, override check from superclass """
- pass
-
-
-directives.register_directive('listing', Listing)
+# -*- 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. + + +""" Define and register a listing directive using the existing CodeBlock """ + + +from __future__ import unicode_literals +from codecs import open as codecs_open # for patching purposes +import os +try: + from urlparse import urlunsplit +except ImportError: + from urllib.parse import urlunsplit # NOQA + +from docutils import core +from docutils import nodes +from docutils.parsers.rst import Directive, directives +from docutils.parsers.rst.directives.misc import Include +try: + from docutils.parsers.rst.directives.body import CodeBlock +except ImportError: # docutils < 0.9 (Debian Sid For The Loss) + class CodeBlock(Directive): + required_arguments = 1 + has_content = True + CODE = '<pre>{0}</pre>' + + def run(self): + """ Required by the Directive interface. Create docutils nodes """ + return [nodes.raw('', self.CODE.format('\n'.join(self.content)), format='html')] + directives.register_directive('code', CodeBlock) + + +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + + name = "rest_listing" + + def set_site(self, site): + self.site = site + # Even though listings don't use CodeBlock anymore, I am + # leaving these to make the code directive work with + # docutils < 0.9 + directives.register_directive('code-block', CodeBlock) + directives.register_directive('sourcecode', CodeBlock) + directives.register_directive('listing', Listing) + return super(Plugin, self).set_site(site) + + +class Listing(Include): + """ listing directive: create a highlighted block of code from a file in listings/ + + Usage: + + .. listing:: nikola.py python + :number-lines: + + """ + has_content = False + required_arguments = 1 + optional_arguments = 1 + + def run(self): + fname = self.arguments.pop(0) + lang = self.arguments.pop(0) + fpath = os.path.join('listings', fname) + self.arguments.insert(0, fpath) + self.options['code'] = lang + with codecs_open(fpath, 'rb+', 'utf8') as fileobject: + self.content = fileobject.read().splitlines() + self.state.document.settings.record_dependencies.add(fpath) + target = urlunsplit(("link", 'listing', fname, '', '')) + generated_nodes = ( + [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]]) + generated_nodes += self.get_code_from_file(fileobject) + return generated_nodes + + def get_code_from_file(self, data): + """ Create CodeBlock nodes from file object content """ + return super(Listing, self).run() + + def assert_has_content(self): + """ Listing has no content, override check from superclass """ + pass diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin new file mode 100644 index 0000000..5f5276b --- /dev/null +++ b/nikola/plugins/compile/rest/media.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_media +Module = media + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Directive to support oembed via micawber + diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py new file mode 100644 index 0000000..d1930dd --- /dev/null +++ b/nikola/plugins/compile/rest/media.py @@ -0,0 +1,63 @@ +# -*- 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 docutils import nodes +from docutils.parsers.rst import Directive, directives + +try: + import micawber +except ImportError: + micawber = None # NOQA + + +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + + +class Plugin(RestExtension): + + name = "rest_media" + + def set_site(self, site): + self.site = site + directives.register_directive('media', Media) + return super(Plugin, self).set_site(site) + + +class Media(Directive): + """ Restructured text extension for inserting any sort of media using micawber.""" + has_content = False + required_arguments = 1 + optional_arguments = 999 + + def run(self): + if micawber is None: + msg = req_missing(['micawber'], 'use the media directive', optional=True) + return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] + + providers = micawber.bootstrap_basic() + return [nodes.raw('', micawber.parse_text(" ".join(self.arguments), providers), format='html')] diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin new file mode 100644 index 0000000..82450a0 --- /dev/null +++ b/nikola/plugins/compile/rest/post_list.plugin @@ -0,0 +1,9 @@ +[Core] +Name = rest_post_list +Module = post_list + +[Documentation] +Author = Udo Spallek +Version = 0.1 +Website = http://getnikola.com +Description = Includes a list of posts with tag and slide based filters. diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py new file mode 100644 index 0000000..eae4016 --- /dev/null +++ b/nikola/plugins/compile/rest/post_list.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2013 Udo Spallek, 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 uuid + +from docutils import nodes +from docutils.parsers.rst import Directive, directives + +from nikola import utils +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + name = "rest_post_list" + + def set_site(self, site): + self.site = site + directives.register_directive('post-list', PostList) + PostList.site = site + return super(Plugin, self).set_site(site) + + +class PostList(Directive): + """ + Post List + ========= + :Directive Arguments: None. + :Directive Options: lang, start, stop, reverse, tags, template, id + :Directive Content: None. + + Provides a reStructuredText directive to create a list of posts. + The posts appearing in the list can be filtered by options. + *List slicing* is provided with the *start*, *stop* and *reverse* options. + + The following not required options are recognized: + + ``start`` : integer + The index of the first post to show. + A negative value like ``-3`` will show the *last* three posts in the + post-list. + Defaults to None. + + ``stop`` : integer + The index of the last post to show. + A value negative value like ``-1`` will show every post, but not the + *last* in the post-list. + Defaults to None. + + ``reverse`` : flag + Reverse the order of the post-list. + Defaults is to not reverse the order of posts. + + ``tags`` : string [, string...] + Filter posts to show only posts having at least one of the ``tags``. + Defaults to None. + + ``slugs`` : string [, string...] + Filter posts to show only posts having at least one of the ``slugs``. + Defaults to None. + + ``all`` : flag + Shows all posts and pages in the post list. + Defaults to show only posts with set *use_in_feeds*. + + ``lang`` : string + The language of post *titles* and *links*. + Defaults to default language. + + ``template`` : string + The name of an alternative template to render the post-list. + Defaults to ``post_list_directive.tmpl`` + + ``id`` : string + A manual id for the post list. + Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex. + """ + option_spec = { + 'start': int, + 'stop': int, + 'reverse': directives.flag, + 'tags': directives.unchanged, + 'slugs': directives.unchanged, + 'all': directives.flag, + 'lang': directives.unchanged, + 'template': directives.path, + 'id': directives.unchanged, + } + + def run(self): + start = self.options.get('start') + stop = self.options.get('stop') + reverse = self.options.get('reverse', False) + tags = self.options.get('tags') + tags = [t.strip().lower() for t in tags.split(',')] if tags else [] + slugs = self.options.get('slugs') + slugs = [s.strip() for s in slugs.split(',')] if slugs else [] + show_all = self.options.get('all', False) + lang = self.options.get('lang', utils.LocaleBorg().current_lang) + template = self.options.get('template', 'post_list_directive.tmpl') + post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex) + + posts = [] + step = -1 if reverse is None else None + if show_all is None: + timeline = [p for p in self.site.timeline] + else: + timeline = [p for p in self.site.timeline if p.use_in_feeds] + + for post in timeline[start:stop:step]: + if tags: + cont = True + for tag in tags: + if tag in [t.lower() for t in post.tags]: + cont = False + + if cont: + continue + + if slugs: + cont = True + for slug in slugs: + if slug == post.meta('slug'): + cont = False + + if cont: + continue + + posts += [post] + + if not posts: + return [] + + template_data = { + 'lang': lang, + 'posts': posts, + 'date_format': self.site.GLOBAL_CONTEXT.get('date_format'), + 'post_list_id': post_list_id, + } + output = self.site.template_system.render_template( + template, None, template_data) + return [nodes.raw('', output, format='html')] diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin new file mode 100644 index 0000000..cee4b06 --- /dev/null +++ b/nikola/plugins/compile/rest/slides.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_slides +Module = slides + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Slides directive + diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile/rest/slides.py index 57fb754..41c3314 100644 --- a/nikola/plugins/compile_rest/slides.py +++ b/nikola/plugins/compile/rest/slides.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -24,9 +26,24 @@ from __future__ import unicode_literals +import uuid + from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + + name = "rest_slides" + + def set_site(self, site): + self.site = site + directives.register_directive('slides', Slides) + Slides.site = site + return super(Plugin, self).set_site(site) + class Slides(Directive): """ Restructured text extension for inserting slideshows.""" @@ -35,31 +52,16 @@ class Slides(Directive): def run(self): if len(self.content) == 0: return - output = [] - output.append(""" - <div id="myCarousel" class="carousel slide"> - <ol class="carousel-indicators"> - """) - for i in range(len(self.content)): - if i == 0: - classname = 'class="active"' - else: - classname = '' - output.append(' <li data-target="#myCarousel" data-slide-to="{0}" {1}></li>'.format(i, classname)) - output.append("""</ol> - <div class="carousel-inner"> - """) - for i, image in enumerate(self.content): - if i == 0: - classname = "item active" - else: - classname = "item" - output.append("""<div class="{0}"><img src="{1}" alt="" style="margin: 0 auto 0 auto;"></div>""".format(classname, image)) - output.append("""</div> - <a class="left carousel-control" href="#myCarousel" data-slide="prev">‹</a> - <a class="right carousel-control" href="#myCarousel" data-slide="next">›</a> - </div>""") - return [nodes.raw('', '\n'.join(output), format='html')] + + output = self.site.template_system.render_template( + 'slides.tmpl', + None, + { + 'content': self.content, + 'carousel_id': 'slides_' + uuid.uuid4().hex, + } + ) + return [nodes.raw('', output, format='html')] directives.register_directive('slides', Slides) diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin new file mode 100644 index 0000000..1d31a8f --- /dev/null +++ b/nikola/plugins/compile/rest/soundcloud.plugin @@ -0,0 +1,10 @@ +[Core] +Name = rest_soundcloud +Module = soundcloud + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Soundcloud directive + diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py index 6bdd4d5..6fb3e99 100644 --- a/nikola/plugins/compile_rest/soundcloud.py +++ b/nikola/plugins/compile/rest/soundcloud.py @@ -1,10 +1,23 @@ -# coding: utf8 +# -*- coding: utf-8 -*- from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + + name = "rest_soundcloud" + + def set_site(self, site): + self.site = site + directives.register_directive('soundcloud', SoundCloud) + return super(Plugin, self).set_site(site) + + CODE = ("""<iframe width="{width}" height="{height}" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/""" @@ -45,6 +58,3 @@ class SoundCloud(Directive): raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " "use ':key: value' instead") - - -directives.register_directive('soundcloud', SoundCloud) diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin new file mode 100644 index 0000000..e0ff3f1 --- /dev/null +++ b/nikola/plugins/compile/rest/vimeo.plugin @@ -0,0 +1,7 @@ +[Core] +Name = rest_vimeo +Module = vimeo + +[Documentation] +Description = Vimeo directive + diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index c1dc143..6d66648 100644 --- a/nikola/plugins/compile_rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -1,5 +1,6 @@ -# coding: utf8 -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -31,13 +32,21 @@ try: import requests except ImportError: requests = None # NOQA -try: - import json # python 2.6 or higher -except ImportError: - try: - import simplejson as json # NOQA - except ImportError: - json = None +import json + + +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + + +class Plugin(RestExtension): + + name = "rest_vimeo" + + def set_site(self, site): + self.site = site + directives.register_directive('vimeo', Vimeo) + return super(Plugin, self).set_site(site) CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}" @@ -77,18 +86,19 @@ class Vimeo(Directive): 'height': VIDEO_DEFAULT_HEIGHT, } if self.request_size: - self.check_modules() + err = self.check_modules() + if err: + return err self.set_video_size() options.update(self.options) return [nodes.raw('', CODE.format(**options), format='html')] def check_modules(self): + msg = None if requests is None: - raise Exception("To use the Vimeo directive you need to install " - "the requests module.") - if json is None: - raise Exception("To use the Vimeo directive you need python 2.6 " - "or to install the simplejson module.") + msg = req_missing(['requests'], 'use the vimeo directive', optional=True) + return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] + return None def set_video_size(self): # Only need to make a connection if width and height aren't provided @@ -113,6 +123,3 @@ class Vimeo(Directive): raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " "use ':key: value' instead") - - -directives.register_directive('vimeo', Vimeo) diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin new file mode 100644 index 0000000..01275be --- /dev/null +++ b/nikola/plugins/compile/rest/youtube.plugin @@ -0,0 +1,8 @@ +[Core] +Name = rest_youtube +Module = youtube + +[Documentation] +Version = 0.1 +Description = Youtube directive + diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile/rest/youtube.py index 767be32..3d4bdd3 100644 --- a/nikola/plugins/compile_rest/youtube.py +++ b/nikola/plugins/compile/rest/youtube.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,6 +28,19 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension + + +class Plugin(RestExtension): + + name = "rest_youtube" + + def set_site(self, site): + self.site = site + directives.register_directive('youtube', Youtube) + return super(Plugin, self).set_site(site) + + CODE = """\ <iframe width="{width}" height="{height}" @@ -64,6 +79,3 @@ class Youtube(Directive): raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " "use ':key: value' instead") - - -directives.register_directive('youtube', Youtube) diff --git a/nikola/plugins/compile_textile.plugin b/nikola/plugins/compile/textile.plugin index c13b3b1..6439b0f 100644 --- a/nikola/plugins/compile_textile.plugin +++ b/nikola/plugins/compile/textile.plugin @@ -1,10 +1,10 @@ [Core] Name = textile -Module = compile_textile +Module = textile [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile Textile into HTML diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile/textile.py index 85efd3f..b402329 100644 --- a/nikola/plugins/compile_textile.py +++ b/nikola/plugins/compile/textile.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,6 +28,7 @@ import codecs import os +import re try: from textile import textile @@ -33,6 +36,7 @@ except ImportError: textile = None # NOQA from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing class CompileTextile(PageCompiler): @@ -40,17 +44,15 @@ class CompileTextile(PageCompiler): name = "textile" - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if textile is None: - raise Exception('To build this site, you need to install the ' - '"textile" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['textile'], 'build this site (compile Textile)') + makedirs(os.path.dirname(dest)) with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] output = textile(data, head_offset=1) out_file.write(output) @@ -58,9 +60,7 @@ class CompileTextile(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<notextile> <!--\n') diff --git a/nikola/plugins/compile_txt2tags.plugin b/nikola/plugins/compile/txt2tags.plugin index 2c65da1..55eb0a0 100644 --- a/nikola/plugins/compile_txt2tags.plugin +++ b/nikola/plugins/compile/txt2tags.plugin @@ -1,10 +1,10 @@ [Core] Name = txt2tags -Module = compile_txt2tags +Module = txt2tags [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile Txt2tags into HTML diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile/txt2tags.py index 001da6e..2f62f04 100644 --- a/nikola/plugins/compile_txt2tags.py +++ b/nikola/plugins/compile/txt2tags.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -39,6 +41,7 @@ except ImportError: txt2tags = None # NOQA from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing class CompileTextile(PageCompiler): @@ -46,14 +49,10 @@ class CompileTextile(PageCompiler): name = "txt2tags" - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if txt2tags is None: - raise Exception('To build this site, you need to install the ' - '"txt2tags" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['txt2tags'], 'build this site (compile txt2tags)') + makedirs(os.path.dirname(dest)) cmd = ["-t", "html", "--no-headers", "--outfile", dest, source] txt2tags(cmd) @@ -61,9 +60,7 @@ class CompileTextile(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write("\n'''\n<!--\n") diff --git a/nikola/plugins/compile_wiki.plugin b/nikola/plugins/compile/wiki.plugin index 65cd942..eee14a8 100644 --- a/nikola/plugins/compile_wiki.plugin +++ b/nikola/plugins/compile/wiki.plugin @@ -1,10 +1,10 @@ [Core] Name = wiki -Module = compile_wiki +Module = wiki [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Compile WikiMarkup into HTML diff --git a/nikola/plugins/compile_wiki.py b/nikola/plugins/compile/wiki.py index fb9e010..b2c4afc 100644 --- a/nikola/plugins/compile_wiki.py +++ b/nikola/plugins/compile/wiki.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -22,7 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on textile.""" +"""Implementation of compile_html based on CreoleWiki.""" import codecs import os @@ -35,21 +37,18 @@ except ImportError: creole = None from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing -class CompileTextile(PageCompiler): - """Compile textile into HTML.""" +class CompileWiki(PageCompiler): + """Compile CreoleWiki into HTML.""" name = "wiki" - def compile_html(self, source, dest): + def compile_html(self, source, dest, is_two_file=True): if creole is None: - raise Exception('To build this site, you need to install the ' - '"creole" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass + req_missing(['creole'], 'build this site (compile CreoleWiki)') + makedirs(os.path.dirname(dest)) with codecs.open(dest, "w+", "utf8") as out_file: with codecs.open(source, "r", "utf8") as in_file: data = in_file.read() @@ -61,9 +60,7 @@ class CompileTextile(PageCompiler): metadata = {} metadata.update(self.default_metadata) metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) + makedirs(os.path.dirname(path)) if onefile: raise Exception('There are no comments in CreoleWiki markup, so ' 'one-file format is not possible, use the -2 ' diff --git a/nikola/plugins/compile_ipynb/README.txt b/nikola/plugins/compile_ipynb/README.txt deleted file mode 100644 index 2cfd45e..0000000 --- a/nikola/plugins/compile_ipynb/README.txt +++ /dev/null @@ -1,35 +0,0 @@ -To make this work... - -1- First, you have to put this plugin in your_site/plugins/ folder. - -2- Then, you have to download the custom nbconvert from here: https://github.com/damianavila/compile_ipynb-for-Nikola.git -and put it inside your_site/plugins/compile_ipynb/ folder - -3- Also, you have to use the site-ipython theme (or make a new one containing the ipython css, mathjax.js and the proper template). -You can get it here: https://github.com/damianavila/site-ipython-theme-for-Nikola - -4- Finally, you have to put: - -post_pages = ( - ("posts/*.ipynb", "posts", "post.tmpl", True), - ("stories/*.ipynb", "stories", "story.tmpl", False), -) - -in your conf.py - -Then... to use it: - -$nikola new_page -f ipynb - -**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler -create one metadata file and the corresponding naive IPython notebook. - -$nikola build - -And deploy the output folder... to see it locally: $nikola serve - -If you have any doubts, just ask: @damianavila - -Cheers. - -Damián diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py deleted file mode 100644 index 3d41571..0000000 --- a/nikola/plugins/compile_rest/__init__.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 os - -try: - import docutils.core - import docutils.io - from docutils.parsers.rst import directives - - from .listing import Listing, CodeBlock - directives.register_directive('code-block', CodeBlock) - directives.register_directive('sourcecode', CodeBlock) - directives.register_directive('listing', Listing) - from .youtube import Youtube - directives.register_directive('youtube', Youtube) - from .vimeo import Vimeo - directives.register_directive('vimeo', Vimeo) - from .slides import Slides - directives.register_directive('slides', Slides) - from .gist_directive import GitHubGist - directives.register_directive('gist', GitHubGist) - from .soundcloud import SoundCloud - directives.register_directive('soundcloud', SoundCloud) - has_docutils = True -except ImportError: - has_docutils = False - -from nikola.plugin_categories import PageCompiler - - -class CompileRest(PageCompiler): - """Compile reSt into HTML.""" - - name = "rest" - - def compile_html(self, source, dest): - """Compile reSt into HTML.""" - if not has_docutils: - raise Exception('To build this site, you need to install the ' - '"docutils" package.') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass - error_level = 100 - with codecs.open(dest, "w+", "utf8") as out_file: - with codecs.open(source, "r", "utf8") as in_file: - data = in_file.read() - output, error_level, deps = rst2html( - data, settings_overrides={ - 'initial_header_level': 2, - 'record_dependencies': True, - 'stylesheet_path': None, - 'link_stylesheet': True, - 'syntax_highlight': 'short', - }) - out_file.write(output) - deps_path = dest + '.dep' - if deps.list: - with codecs.open(deps_path, "wb+", "utf8") as deps_file: - deps_file.write('\n'.join(deps.list)) - else: - if os.path.isfile(deps_path): - os.unlink(deps_path) - if error_level < 3: - return True - else: - return False - - def create_post(self, path, onefile=False, **kw): - metadata = {} - metadata.update(self.default_metadata) - metadata.update(kw) - d_name = os.path.dirname(path) - if not os.path.isdir(d_name): - os.makedirs(os.path.dirname(path)) - with codecs.open(path, "wb+", "utf8") as fd: - if onefile: - for k, v in metadata.items(): - fd.write('.. {0}: {1}\n'.format(k, v)) - fd.write("\nWrite your post here.") - - -def rst2html(source, source_path=None, source_class=docutils.io.StringInput, - destination_path=None, reader=None, reader_name='standalone', - parser=None, parser_name='restructuredtext', writer=None, - writer_name='html', settings=None, settings_spec=None, - settings_overrides=None, config_section=None, - enable_exit_status=None): - """ - Set up & run a `Publisher`, and return a dictionary of document parts. - Dictionary keys are the names of parts, and values are Unicode strings; - encoding is up to the client. For programmatic use with string I/O. - - For encoded string input, be sure to set the 'input_encoding' setting to - the desired encoding. Set it to 'unicode' for unencoded Unicode string - input. Here's how:: - - publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) - - Parameters: see `publish_programmatically`. - """ - output, pub = docutils.core.publish_programmatically( - source=source, source_path=source_path, source_class=source_class, - destination_class=docutils.io.StringOutput, - destination=None, destination_path=destination_path, - reader=reader, reader_name=reader_name, - parser=parser, parser_name=parser_name, - writer=writer, writer_name=writer_name, - settings=settings, settings_spec=settings_spec, - settings_overrides=settings_overrides, - config_section=config_section, - enable_exit_status=enable_exit_status) - return pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies diff --git a/nikola/plugins/loghandler/smtp.plugin b/nikola/plugins/loghandler/smtp.plugin new file mode 100644 index 0000000..e914b3d --- /dev/null +++ b/nikola/plugins/loghandler/smtp.plugin @@ -0,0 +1,9 @@ +[Core] +Name = smtp +Module = smtp + +[Documentation] +Author = Daniel Devine +Version = 0.1 +Website = http://getnikola.com +Description = Log over smtp (email). diff --git a/nikola/plugins/loghandler/smtp.py b/nikola/plugins/loghandler/smtp.py new file mode 100644 index 0000000..deb8f4e --- /dev/null +++ b/nikola/plugins/loghandler/smtp.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Daniel Devine 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 nikola.plugin_categories import SignalHandler +from blinker import signal +import logbook + + +class SmtpHandler(SignalHandler): + name = 'smtp' + + def attach_handler(self, sender): + """Add the handler to a list of handlers that are attached when get_logger() is called..""" + smtpconf = self.site.config.get('LOGGING_HANDLERS').get('smtp') + if smtpconf: + smtpconf['format_string'] = '''\ +Subject: {record.level_name}: {record.channel} + +{record.message} +''' + self.site.loghandlers.append(logbook.MailHandler( + smtpconf.pop('from_addr'), + smtpconf.pop('recipients'), + **smtpconf + )) + + def set_site(self, site): + self.site = site + + ready = signal('sighandlers_loaded') + ready.connect(self.attach_handler) diff --git a/nikola/plugins/loghandler/stderr.plugin b/nikola/plugins/loghandler/stderr.plugin new file mode 100644 index 0000000..211d2b4 --- /dev/null +++ b/nikola/plugins/loghandler/stderr.plugin @@ -0,0 +1,9 @@ +[Core] +Name = stderr +Module = stderr + +[Documentation] +Author = Daniel Devine +Version = 0.1 +Website = http://getnikola.com +Description = Log to stderr, the default logger. diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py new file mode 100644 index 0000000..71f1de5 --- /dev/null +++ b/nikola/plugins/loghandler/stderr.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Daniel Devine 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 nikola.plugin_categories import SignalHandler +from blinker import signal +import logbook +import os + + +class StderrHandler(SignalHandler): + """Logs messages to stderr.""" + name = 'stderr' + + def attach_handler(self, sender): + """Attach the handler to the logger.""" + conf = self.site.config.get('LOGGING_HANDLERS').get('stderr') + if conf or os.getenv('NIKOLA_DEBUG'): + self.site.loghandlers.append(logbook.StderrHandler( + level='DEBUG' if os.getenv('NIKOLA_DEBUG') else conf.get('loglevel', 'WARNING').upper(), + format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}' + )) + + def set_site(self, site): + self.site = site + + ready = signal('sighandlers_loaded') + ready.connect(self.attach_handler) 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 index 23f93ed..448b115 100644 --- a/nikola/plugins/task_archive.plugin +++ b/nikola/plugins/task/archive.plugin @@ -1,10 +1,10 @@ [Core] Name = render_archive -Module = task_archive +Module = archive [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index a67826f..3afbea1 100644 --- a/nikola/plugins/task_archive.py +++ b/nikola/plugins/task/archive.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -22,10 +24,10 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import calendar import os -import sys +# 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 @@ -35,6 +37,10 @@ class Archive(Task): 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, @@ -42,16 +48,28 @@ class Archive(Task): "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"]: - for year, posts in self.site.posts_per_year.items(): + 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 - context["title"] = kw["messages"][lang]["Posts for year %s"] % year + 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" @@ -62,8 +80,9 @@ class Archive(Task): 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"] = [[get_month_name(int(month), lang), month] for month in months] + 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, @@ -95,7 +114,7 @@ class Archive(Task): context["permalink"] = self.site.link("archive", year, lang) context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format( - year=year, month=get_month_name(int(month), lang)) + year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang)) task = self.site.generic_post_list_renderer( lang, post_list, @@ -109,39 +128,40 @@ class Archive(Task): task['basename'] = self.name yield task - # And global "all your years" page - 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 - + 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 get_month_name(month_no, locale): - if sys.version_info[0] == 3: # Python 3 - with calendar.different_locale((locale, "UTF-8")): - s = calendar.month_name[month_no] - else: # Python 2 - with calendar.TimeEncoding((locale, "UTF-8")): - s = calendar.month_name[month_no] - return s + 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_create_bundles.plugin b/nikola/plugins/task/bundles.plugin index 5d4f6d3..e0b0a4d 100644 --- a/nikola/plugins/task_create_bundles.plugin +++ b/nikola/plugins/task/bundles.plugin @@ -1,10 +1,10 @@ [Core] Name = create_bundles -Module = task_create_bundles +Module = bundles [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Theme bundles using WebAssets diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task/bundles.py index 84ac0ab..488f96f 100644 --- a/nikola/plugins/task_create_bundles.py +++ b/nikola/plugins/task/bundles.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -38,7 +40,7 @@ from nikola import utils class BuildBundles(LateTask): """Bundle assets using WebAssets.""" - name = "build_bundles" + name = "create_bundles" def set_site(self, site): super(BuildBundles, self).set_site(site) @@ -64,44 +66,38 @@ class BuildBundles(LateTask): 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') - if not os.path.isdir(cache_dir): - os.makedirs(cache_dir) + utils.makedirs(cache_dir) env = webassets.Environment(out_dir, os.path.dirname(output), cache=cache_dir) - bundle = webassets.Bundle(*inputs, output=os.path.basename(output)) - env.register(output, bundle) - # This generates the file - env[output].urls() + 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 - flag = False + 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 = [utils.get_asset_path( - os.path.join(dname, fname), kw['themes'], - kw['files_folders']) - for fname in files - ] - file_dep = filter(None, file_dep) # removes missing files + 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': file_dep, + '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)] + 'uptodate': [utils.config_changed(kw)], + 'clean': True, } - flag = True yield utils.apply_filters(task, kw['filters']) - if flag is False: # No page rendered, yield a dummy task - yield { - 'basename': self.name, - 'uptodate': [True], - 'name': 'None', - 'actions': [], - } def get_theme_bundles(themes): diff --git a/nikola/plugins/task_copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin index b11133f..28b9e32 100644 --- a/nikola/plugins/task_copy_assets.plugin +++ b/nikola/plugins/task/copy_assets.plugin @@ -1,10 +1,10 @@ [Core] Name = copy_assets -Module = task_copy_assets +Module = copy_assets [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index 06d17e7..f3d85df 100644 --- a/nikola/plugins/task_copy_assets.py +++ b/nikola/plugins/task/copy_assets.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -47,10 +49,12 @@ class CopyAssets(Task): "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], } - flag = True 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') @@ -62,22 +66,14 @@ class CopyAssets(Task): tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw)] task['basename'] = self.name - flag = False yield utils.apply_filters(task, kw['filters']) - if flag: - yield { - 'basename': self.name, - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - 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;}") diff --git a/nikola/plugins/task_copy_files.plugin b/nikola/plugins/task/copy_files.plugin index 0bfc5be..45c9e0d 100644 --- a/nikola/plugins/task_copy_files.plugin +++ b/nikola/plugins/task/copy_files.plugin @@ -1,10 +1,10 @@ [Core] Name = copy_files -Module = task_copy_files +Module = copy_files [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index feaf147..88e89eb 100644 --- a/nikola/plugins/task_copy_files.py +++ b/nikola/plugins/task/copy_files.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -42,18 +44,12 @@ class CopyFiles(Task): 'filters': self.site.config['FILTERS'], } - flag = False + 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): - flag = True task['basename'] = self.name task['uptodate'] = [utils.config_changed(kw)] yield utils.apply_filters(task, filters) - if not flag: - yield { - 'basename': self.name, - 'actions': (), - } diff --git a/nikola/plugins/task_render_galleries.plugin b/nikola/plugins/task/galleries.plugin index e0a86c0..8352151 100644 --- a/nikola/plugins/task_render_galleries.plugin +++ b/nikola/plugins/task/galleries.plugin @@ -1,10 +1,10 @@ [Core] Name = render_galleries -Module = task_render_galleries +Module = galleries [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index 1536006..a18942c 100644 --- a/nikola/plugins/task_indexes.plugin +++ b/nikola/plugins/task/indexes.plugin @@ -1,10 +1,10 @@ [Core] -Name = render_index -Module = task_indexes +Name = render_indexes +Module = indexes [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index aa5e648..0d20422 100644 --- a/nikola/plugins/task_indexes.py +++ b/nikola/plugins/task/indexes.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -24,6 +26,7 @@ from __future__ import unicode_literals import glob +import itertools import os from nikola.plugin_categories import Task @@ -35,8 +38,13 @@ class Indexes(Task): 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'], @@ -54,8 +62,6 @@ class Indexes(Task): template_name = "index.tmpl" posts = [x for x in self.site.timeline if x.use_in_feeds] - if not posts: - yield {'basename': 'render_indexes', 'actions': []} for lang in kw["translations"]: # Split in smaller lists lists = [] @@ -63,31 +69,40 @@ class Indexes(Task): 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"]:] + 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 not i: - context["title"] = indexes_title + if kw["indexes_pages"]: + indexes_pages = kw["indexes_pages"] % i else: - if kw["indexes_pages"]: - indexes_pages = kw["indexes_pages"] % i - else: - indexes_pages = " (" + \ - kw["messages"][lang]["old posts page %d"] % i + ")" + 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 > 1: - context["prevlink"] = "index-{0}.html".format(i - 1) - if i == 1: - context["prevlink"] = "index.html" - if i < num_pages - 1: - context["nextlink"] = "index-{0}.html".format(i + 1) + 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, @@ -115,15 +130,15 @@ class Indexes(Task): } template_name = "list.tmpl" for lang in kw["translations"]: - for wildcard, dest, _, is_post in kw["post_pages"]: - if is_post: - continue + # 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 = glob.glob(wildcard) - post_list = [self.site.global_data[os.path.splitext(p)[0]] for - p in files] + 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, @@ -135,7 +150,18 @@ class Indexes(Task): 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 + 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_render_listings.plugin b/nikola/plugins/task/listings.plugin index 1f897b9..c93184d 100644 --- a/nikola/plugins/task_render_listings.plugin +++ b/nikola/plugins/task/listings.plugin @@ -1,10 +1,10 @@ [Core] Name = render_listings -Module = task_render_listings +Module = listings [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Render code listings into output diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task/listings.py index 0cadfd3..ab62e74 100644 --- a/nikola/plugins/task_render_listings.py +++ b/nikola/plugins/task/listings.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -39,12 +41,17 @@ class Listings(Task): 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 @@ -60,7 +67,7 @@ class Listings(Task): code = highlight(fd.read(), lexer, HtmlFormatter(cssclass='code', linenos="table", nowrap=False, - lineanchors=utils.slugify(f), + lineanchors=utils.slugify(in_name), anchorlinenos=True)) title = os.path.basename(in_name) else: @@ -80,14 +87,15 @@ class Listings(Task): } self.site.render_template('listing.tmpl', out_name, context) - flag = True + + yield self.group_task() + template_deps = self.site.template_system.template_deps('listing.tmpl') for root, dirs, files in os.walk(kw['listings_folder']): - flag = False # Render all files out_name = os.path.join( kw['output_folder'], - root, 'index.html' + root, kw['index_file'] ) yield { 'basename': self.name, @@ -98,7 +106,7 @@ class Listings(Task): # This is necessary to reflect changes in blog title, # sidebar links, etc. 'uptodate': [utils.config_changed( - self.site.config['GLOBAL_CONTEXT'])], + self.site.GLOBAL_CONTEXT)], 'clean': True, } for f in files: @@ -119,11 +127,10 @@ class Listings(Task): # This is necessary to reflect changes in blog title, # sidebar links, etc. 'uptodate': [utils.config_changed( - self.site.config['GLOBAL_CONTEXT'])], + self.site.GLOBAL_CONTEXT)], 'clean': True, } - if flag: - yield { - 'basename': self.name, - 'actions': [], - } + + 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 index 33eb78b..86accb6 100644 --- a/nikola/plugins/task_localsearch.plugin +++ b/nikola/plugins/task/localsearch.plugin @@ -1,10 +1,10 @@ [Core] Name = local_search -Module = task_localsearch +Module = localsearch [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index f131068..f131068 100644 --- a/nikola/plugins/task_localsearch/MIT-LICENSE.txt +++ b/nikola/plugins/task/localsearch/MIT-LICENSE.txt diff --git a/nikola/plugins/task_localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py index db8610a..9162604 100644 --- a/nikola/plugins/task_localsearch/__init__.py +++ b/nikola/plugins/task/localsearch/__init__.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -27,8 +29,10 @@ 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 +from nikola.utils import config_changed, copy_tree, makedirs # This is what we need to produce: #var tipuesearch = {"pages": [ @@ -68,7 +72,7 @@ class Tipue(LateTask): for lang in kw["translations"]: for post in posts: # Don't index drafts (Issue #387) - if post.is_draft: + if post.is_draft or post.is_retired or post.publish_later: continue text = post.text(lang, strip_html=True) text = text.replace('^', '') @@ -80,10 +84,7 @@ class Tipue(LateTask): data["loc"] = post.permalink(lang) pages.append(data) output = json.dumps({"pages": pages}, indent=2) - try: - os.makedirs(os.path.dirname(dst_path)) - except: - pass + makedirs(os.path.dirname(dst_path)) with codecs.open(dst_path, "wb+", "utf8") as fd: fd.write(output) @@ -92,8 +93,11 @@ class Tipue(LateTask): "name": dst_path, "targets": [dst_path], "actions": [(save_data, [])], - 'uptodate': [config_changed(kw)] + '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") 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 differindex 9c97738..9c97738 100644 --- a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif +++ 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 index 5c766ea..a9982cd 100644 --- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js +++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js @@ -1,10 +1,10 @@ /* -Tipue Search 2.1 +Tipue Search 3.0.1 Copyright (c) 2013 Tipue Tipue Search is released under the MIT License http://www.tipue.com/search -*/ +*/ (function($) { @@ -12,7 +12,7 @@ http://www.tipue.com/search $.fn.tipuesearch = function(options) { var set = $.extend( { - + 'show' : 7, 'newWindow' : false, 'showURL' : true, @@ -24,9 +24,9 @@ http://www.tipue.com/search 'liveDescription' : '*', 'liveContent' : '*', 'contentLocation' : 'tipuesearch/tipuesearch_content.json' - + }, options); - + return this.each(function() { var tipuesearch_in = { @@ -47,7 +47,7 @@ http://www.tipue.com/search 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) @@ -63,13 +63,13 @@ http://www.tipue.com/search "title": tit, "text": desc, "tags": cont, - "loc": tipuesearch_pages[i] - }); + "loc": tipuesearch_pages[i] + }); } ); } } - + if (set.mode == 'json') { $.getJSON(set.contentLocation, @@ -80,15 +80,15 @@ http://www.tipue.com/search ); } - if (set.mode == 'static' || set.mode == 'static-images') + if (set.mode == 'static') { tipuesearch_in = $.extend({}, tipuesearch); - } - + } + var tipue_search_w = ''; if (set.newWindow) { - tipue_search_w = ' target="_blank"'; + tipue_search_w = ' target="_blank"'; } function getURLP(name) @@ -99,8 +99,8 @@ http://www.tipue.com/search { $('#tipue_search_input').val(getURLP('q')); getTipueSearch(0, true); - } - + } + $('#tipue_search_button').click(function() { getTipueSearch(0, true); @@ -120,7 +120,7 @@ http://www.tipue.com/search 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(' '); @@ -133,7 +133,7 @@ http://www.tipue.com/search if (d_w[i] == tipuesearch_stop_words[f]) { a_w = false; - show_stop = true; + show_stop = true; } } if (a_w) @@ -143,7 +143,7 @@ http://www.tipue.com/search } d = $.trim(d); d_w = d.split(' '); - + if (d.length >= set.minimumLength) { if (replace) @@ -161,8 +161,8 @@ http://www.tipue.com/search } } d_w = d.split(' '); - } - + } + var d_t = d; for (var i = 0; i < d_w.length; i++) { @@ -193,10 +193,10 @@ http://www.tipue.com/search { score -= (150000 - i); } - + if (set.highlightTerms) { - if (set.highlightEveryTerm) + if (set.highlightEveryTerm) { var patr = new RegExp('(' + d_w[f] + ')', 'gi'); } @@ -204,33 +204,26 @@ http://www.tipue.com/search { var patr = new RegExp('(' + d_w[f] + ')', 'i'); } - s_t = s_t.replace(patr, "<em>$1</em>"); + s_t = s_t.replace(patr, "<b>$1</b>"); } - if (tipuesearch_in.pages[i].tags.search(pat) != -1) { score -= (100000 - i); - } + } + } if (score < 1000000000) { - if (set.mode == 'static-images') - { - found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc + '^' + tipuesearch_in.pages[i].image; - } - else - { - found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc; - } + 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">Show results for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></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) { @@ -241,7 +234,7 @@ http://www.tipue.com/search 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++) @@ -250,16 +243,7 @@ http://www.tipue.com/search 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>'; - - if (set.mode == 'static-images') - { - if (fo[4]) - { - out += '<div class="tipue_search_content_image_box"><img class="tipue_search_content_image" id="' + fo[1] + '^' + fo[3] + '" '; - out += 'src=' + fo[4] + '></div><div style="clear: both;"></div>'; - } - } - + var t = fo[2]; var t_d = ''; var t_w = t.split(' '); @@ -271,7 +255,7 @@ http://www.tipue.com/search { for (var f = 0; f < set.descriptiveWords; f++) { - t_d += t_w[f] + ' '; + t_d += t_w[f] + ' '; } } t_d = $.trim(t_d); @@ -280,33 +264,38 @@ http://www.tipue.com/search t_d += ' ...'; } out += '<div class="tipue_search_content_text">' + t_d + '</div>'; - + if (set.showURL) { - out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[3] + '</a></div>'; + 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++; + 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 + '">« Previous</a></li>'; + out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">Prev</a></li>'; } - - if (page <= 4) + + if (page <= 2) { var p_b = pages; - if (pages > 5) + if (pages > 3) { - p_b = 5; - } + p_b = 3; + } for (var f = 0; f < p_b; f++) { if (f == page) @@ -321,10 +310,10 @@ http://www.tipue.com/search } else { - var p_b = pages + 4; + var p_b = page + 3; if (p_b > pages) { - p_b = pages; + p_b = pages; } for (var f = page; f < p_b; f++) { @@ -336,27 +325,27 @@ http://www.tipue.com/search { 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 += '<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>'; + 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>'; + out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>'; } else { @@ -371,56 +360,25 @@ http://www.tipue.com/search } } } - + $('#tipue_search_content').html(out); $('#tipue_search_content').slideDown(200); - + $('#tipue_search_replaced').click(function() { getTipueSearch(0, false); }); - - $('.tipue_search_content_image').click(function() - { - var src_i = $(this).attr('src'); - var id_v = $(this).attr('id'); - var id_a = id_v.split('^'); - - var l_c_i = '<img src="' + src_i + '"><div id="tipue_lightbox_content_title">' + id_a[0] + '</div>'; - l_c_i += '<a href="' + id_a[1] + '"' + tipue_search_w + '><div id="tipue_lightbox_content_link"></div></a>'; - l_c_i += '<a href="' + src_i + '"' + tipue_search_w + '><div id="tipue_lightbox_content_expand"></div></a><div style="clear: both;"></div>'; - - if ($('#tipue_lightbox').length > 0) - { - $('#tipue_lightbox_content').html(l_c_i); - $('#tipue_lightbox').fadeIn(); - } - else - { - var tipue_lightbox = '<div id="tipue_lightbox">' + '<div id="tipue_lightbox_content">' + l_c_i + '</div></div>'; - $('body').append(tipue_lightbox); - $('#tipue_lightbox').fadeIn(); - } - }); - $('#tipue_lightbox').live('click', function() - { - $('#tipue_lightbox').hide(); - }); - + $('.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); - - - +})(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 index 8989c3c..8493ec1 100644 --- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js +++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js @@ -1,7 +1,7 @@ /* -Tipue Search 2.0 -Copyright (c) 2012 Tipue +Tipue Search 3.0.1 +Copyright (c) 2013 Tipue Tipue Search is released under the MIT License http://www.tipue.com/search */ @@ -19,10 +19,3 @@ var tipuesearch_stem = {"words": [ {"word": "javascript", stem: "script"}, {"word": "javascript", stem: "js"} ]}; - -/* -Include the following variable listing the pages on your site if you're using Live mode -*/ - -var tipuesearch_pages = ["http://foo.com/", "http://foo.com/about/", "http://foo.com/blog/", "http://foo.com/tos/"]; - diff --git a/nikola/plugins/task_localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html index 789fbe5..789fbe5 100755 --- a/nikola/plugins/task_localsearch/files/tipue_search.html +++ b/nikola/plugins/task/localsearch/files/tipue_search.html diff --git a/nikola/plugins/task_mustache.plugin b/nikola/plugins/task/mustache.plugin index 6103936..d6b487a 100644 --- a/nikola/plugins/task_mustache.plugin +++ b/nikola/plugins/task/mustache.plugin @@ -1,10 +1,10 @@ [Core] Name = render_mustache -Module = task_mustache +Module = mustache [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index 7364979..c392e3b 100644 --- a/nikola/plugins/task_mustache/__init__.py +++ b/nikola/plugins/task/mustache/__init__.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -29,7 +31,7 @@ import json import os from nikola.plugin_categories import Task -from nikola.utils import config_changed, copy_file, unicode_str +from nikola.utils import config_changed, copy_file, unicode_str, makedirs class Mustache(Task): @@ -103,25 +105,11 @@ class Mustache(Task): post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']), }) - # Disqus comments - data["disqus_html"] = ('<div id="disqus_thread"></div> <script ' - 'type="text/javascript">var disqus_' - 'shortname="%s";var disqus_url="%s";' - '(function(){var a=document.createElement' - '("script");a.type="text/javascript";' - 'a.async=true;a.src="http://"+disqus_' - 'shortname+".disqus.com/embed.js";(' - 'document.getElementsByTagName("head")' - '[0]||document.getElementsByTagName("body")' - '[0]).appendChild(a)})(); </script>' - '<noscript>Please enable JavaScript to view' - ' the <a href="http://disqus.com/' - '?ref_noscript">comments powered by DISQUS.' - '</a></noscript><a href="http://disqus.com"' - 'class="dsq-brlink">comments powered by <sp' - 'an class="logo-disqus">DISQUS</span></a>' % - (self.site.config['DISQUS_FORUM'], - post.permalink(absolute=True))) + # 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 = [] @@ -135,10 +123,7 @@ class Mustache(Task): ".html", ".json")}) data["translations"] = translations - try: - os.makedirs(os.path.dirname(path)) - except: - pass + makedirs(os.path.dirname(path)) with codecs.open(path, 'wb+', 'utf8') as fd: fd.write(json.dumps(data)) diff --git a/nikola/plugins/task_mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html index 7f2b34c..e9a0213 100644 --- a/nikola/plugins/task_mustache/mustache-template.html +++ b/nikola/plugins/task/mustache/mustache-template.html @@ -5,7 +5,7 @@ <hr> <h2>{{title}}</h2> Posted on: {{date}}</br> - {{#tags?}} More posts about: + {{#tags?}} More posts about: {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}} </br> {{/tags?}} @@ -14,13 +14,13 @@ {{{text}}} <ul class="pager"> {{#prev}} - <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li> + <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> - {{{disqus_html}}} + {{{comment_html}}} </div> <div class="footerbox"> {{{CONTENT_FOOTER}}} diff --git a/nikola/plugins/task_mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html index 5dbebef..7ff6312 100644 --- a/nikola/plugins/task_mustache/mustache.html +++ b/nikola/plugins/task/mustache/mustache.html @@ -4,14 +4,12 @@ <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/slides.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.7.2.min.js" type="text/javascript"></script> - <script src="https://raw.github.com/jonnyreeves/jquery-Mustache/master/src/jquery.mustache.js"></script> - <script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script> + <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 src="/assets/js/slides.min.jquery.js" type="text/javascript"></script> <script type="text/javascript"> function load_data(dataurl) { jQuery.getJSON(dataurl, function(data) { @@ -29,7 +27,7 @@ $.Mustache.load('/mustache-template.html') load_data('{{first_post_data}}'); }; }) -}); +}); </script> </head> <body style="padding-top: 0;"> diff --git a/nikola/plugins/task_render_pages.plugin b/nikola/plugins/task/pages.plugin index e2a358c..67212d2 100644 --- a/nikola/plugins/task_render_pages.plugin +++ b/nikola/plugins/task/pages.plugin @@ -1,10 +1,10 @@ [Core] Name = render_pages -Module = task_render_pages +Module = pages [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Create pages in the output. diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task/pages.py index 1883d7b..eb5b49e 100644 --- a/nikola/plugins/task_render_pages.py +++ b/nikola/plugins/task/pages.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -41,7 +43,7 @@ class RenderPages(Task): "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], } self.site.scan_posts() - flag = False + 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): @@ -53,12 +55,4 @@ class RenderPages(Task): 2: kw})] task['basename'] = self.name task['task_dep'] = ['render_posts'] - flag = True yield task - if flag is False: # No page rendered, yield a dummy task - yield { - 'basename': self.name, - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } diff --git a/nikola/plugins/task_render_posts.plugin b/nikola/plugins/task/posts.plugin index 0d19ea9..e1a42fd 100644 --- a/nikola/plugins/task_render_posts.plugin +++ b/nikola/plugins/task/posts.plugin @@ -1,10 +1,10 @@ [Core] Name = render_posts -Module = task_render_posts +Module = posts [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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 index 285720b..826f3d8 100644 --- a/nikola/plugins/task_redirect.plugin +++ b/nikola/plugins/task/redirect.plugin @@ -1,10 +1,10 @@ [Core] Name = redirect -Module = task_redirect +Module = redirect [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Create redirect pages. diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task/redirect.py index 2503bb7..ade878a 100644 --- a/nikola/plugins/task_redirect.py +++ b/nikola/plugins/task/redirect.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -30,7 +32,7 @@ from nikola import utils class Redirect(Task): - """Copy theme assets into output.""" + """Generate redirections""" name = "redirect" @@ -42,17 +44,8 @@ class Redirect(Task): 'output_folder': self.site.config['OUTPUT_FOLDER'], } - if not kw['redirections']: - # If there are no redirections, still needs to create a - # dummy action so dependencies don't fail - yield { - 'basename': self.name, - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - else: + yield self.group_task() + if kw['redirections']: for src, dst in kw["redirections"]: src_path = os.path.join(kw["output_folder"], src) yield { @@ -66,10 +59,7 @@ class Redirect(Task): def create_redirect(src, dst): - try: - os.makedirs(os.path.dirname(src)) - except: - pass + 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; ' diff --git a/nikola/plugins/task_render_rss.plugin b/nikola/plugins/task/rss.plugin index 20caf15..7206a43 100644 --- a/nikola/plugins/task_render_rss.plugin +++ b/nikola/plugins/task/rss.plugin @@ -1,10 +1,10 @@ [Core] -Name = render_rss -Module = task_render_rss +Name = generate_rss +Module = rss [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Generate RSS feeds. diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task/rss.py index 3000e47..bcca4da 100644 --- a/nikola/plugins/task_render_rss.py +++ b/nikola/plugins/task/rss.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -22,16 +24,25 @@ # 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 RenderRSS(Task): +class GenerateRSS(Task): """Generate RSS feeds.""" - name = "render_rss" + 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.""" @@ -44,8 +55,10 @@ class RenderRSS(Task): "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)) @@ -57,16 +70,22 @@ class RenderRSS(Task): 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': 'render_rss', + '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["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 index f6b01d7..2cd8195 100644 --- a/nikola/plugins/task_sitemap.plugin +++ b/nikola/plugins/task/sitemap.plugin @@ -1,10 +1,10 @@ [Core] Name = sitemap -Module = task_sitemap +Module = sitemap [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +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_render_sources.plugin b/nikola/plugins/task/sources.plugin index 5b59598..6224e48 100644 --- a/nikola/plugins/task_render_sources.plugin +++ b/nikola/plugins/task/sources.plugin @@ -1,10 +1,10 @@ [Core] Name = render_sources -Module = task_render_sources +Module = sources [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Copy page sources into the output. diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task/sources.py index 392345c..672f354 100644 --- a/nikola/plugins/task_render_sources.py +++ b/nikola/plugins/task/sources.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -50,35 +52,30 @@ class Sources(Task): } self.site.scan_posts() - flag = False - 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 - if source.endswith('.html'): - 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)], - } - if flag is False: # No page rendered, yield a dummy task - yield { - 'basename': 'render_sources', - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } + 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_render_tags.plugin b/nikola/plugins/task/tags.plugin index b826e87..f01e0f8 100644 --- a/nikola/plugins/task_render_tags.plugin +++ b/nikola/plugins/task/tags.plugin @@ -1,10 +1,10 @@ [Core] Name = render_tags -Module = task_render_tags +Module = tags [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Render the tag pages and feeds. diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task/tags.py index 58a7ff3..299dca4 100644 --- a/nikola/plugins/task_render_tags.py +++ b/nikola/plugins/task/tags.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,16 +28,28 @@ 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 pages and feeds.""" + """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.""" @@ -43,7 +57,6 @@ class RenderTags(Task): "translations": self.site.config["TRANSLATIONS"], "blog_title": self.site.config["BLOG_TITLE"], "site_url": self.site.config["SITE_URL"], - "blog_description": self.site.config["BLOG_DESCRIPTION"], "messages": self.site.MESSAGES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], @@ -53,17 +66,21 @@ class RenderTags(Task): "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: - yield {'basename': str(self.name), 'actions': []} + if not self.site.posts_per_tag and not self.site.posts_per_category: return - for tag, posts in list(self.site.posts_per_tag.items()): + 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() @@ -72,13 +89,23 @@ class RenderTags(Task): filtered_posts = [x for x in post_list if x.is_translation_available(lang)] else: filtered_posts = post_list - rss_post_list = [p.post_name for p in filtered_posts] - yield self.tag_rss(tag, lang, rss_post_list, kw) + 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) + 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) + 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 = {} @@ -89,10 +116,7 @@ class RenderTags(Task): 'assets', 'js', 'tag_cloud_data.json') def write_tag_data(data): - try: - os.makedirs(os.path.dirname(output_name)) - except: - pass + utils.makedirs(os.path.dirname(output_name)) with codecs.open(output_name, 'wb+', 'utf8') as fd: fd.write(json.dumps(data)) @@ -107,21 +131,37 @@ class RenderTags(Task): yield task def list_tags_page(self, kw): - """a global "all your tags" page for each language""" + """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 = {} - context["title"] = kw["messages"][lang]["Tags"] + 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, [], @@ -132,15 +172,18 @@ class RenderTags(Task): ) 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): + 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("tag", tag, lang) + name = self.site.path(kind, tag, lang) if i: name = name.replace('.html', '-{0}.html'.format(i)) return name @@ -159,7 +202,7 @@ class RenderTags(Task): 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("tag_rss", tag, lang))) + 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)) @@ -177,8 +220,9 @@ class RenderTags(Task): if i < num_pages - 1: context["nextlink"] = os.path.basename( page_name(tag, i + 1, lang)) - context["permalink"] = self.site.link("tag", tag, 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, @@ -190,19 +234,23 @@ class RenderTags(Task): 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): + 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( - "tag", tag, lang)) + 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("tag", tag, lang) + 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, @@ -216,11 +264,14 @@ class RenderTags(Task): task['basename'] = str(self.name) yield task - def tag_rss(self, tag, lang, posts, kw): + 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.join(kw['output_folder'], - self.site.path("tag_rss", tag, lang)) + 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] @@ -235,9 +286,39 @@ class RenderTags(Task): 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, (lang, "{0} ({1})".format(kw["blog_title"], tag), - kw["site_url"], kw["blog_description"], post_list, - output_name, kw["rss_teasers"]))], + 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] diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png Binary files differdeleted file mode 100755 index 21bb7b0..0000000 --- a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png +++ /dev/null diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/link.png b/nikola/plugins/task_localsearch/files/assets/css/img/link.png Binary files differdeleted file mode 100755 index d4e51c5..0000000 --- a/nikola/plugins/task_localsearch/files/assets/css/img/link.png +++ /dev/null diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif Binary files differdeleted file mode 100644 index 644bd17..0000000 --- a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif +++ /dev/null diff --git a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css deleted file mode 100755 index 96dadf0..0000000 --- a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css +++ /dev/null @@ -1,232 +0,0 @@ - -/* -Tipue Search 2.1 -Copyright (c) 2013 Tipue -Tipue Search is released under the MIT License -http://www.tipue.com/search -*/ - - -em -{ - font: inherit; - font-weight: 400; -} -#tipue_search_input -{ -} -#tipue_search_input:focus -{ - border-color: #c3c3c3; - box-shadow: 0 0 3px rgba(0,0,0,.2); -} -#tipue_search_button -{ - width: 60px; - height: 33px; - margin-top: 1px; - border: 1px solid #dcdcdc; - border-radius: 2px; - background: #f1f1f1 url('img/search.gif') no-repeat center; - outline: none; -} -#tipue_search_button:hover -{ - border: 1px solid #c3c3c3; - -moz-box-shadow: 1px 1px 2px #e3e3e3; - -webkit-box-shadow: 1px 1px 2px #e3e3e3; - box-shadow: 1px 1px 2px #e3e3e3; -} - -#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: 14px/1.5 'open sans', sans-serif; - color: #333; -} -#tipue_search_warning -{ - font: 300 14px/1.5 lato, sans-serif; - color: #111; - margin: 13px 0; -} -#tipue_search_warning a -{ - color: #36c; - text-decoration: none; -} -#tipue_search_warning a:hover -{ - color: #111; -} - -#tipue_search_results_count -{ - font: 300 14px/1.5 lato, sans-serif; - color: #111; -} - -.tipue_search_content_title -{ - font: 300 19px/1.5 'open sans', sans-serif; - margin-top: 31px; -} -.tipue_search_content_title a -{ - color: #36c; - text-decoration: none; -} -.tipue_search_content_title a:hover -{ - color: #333; -} - -.tipue_search_content_image_box -{ - float: left; - border: 1px solid #f3f3f3; - padding: 13px; - margin: 21px 0 7px 0; -} -.tipue_search_content_image -{ - max-width: 110px; - height: auto; - outline: none; - cursor: pointer; -} -#tipue_lightbox -{ - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, .9); -} -#tipue_lightbox_content -{ - margin: 37px auto; - background-color: #fff; - padding: 30px; - border: 1px solid #ccc; - width: 250px; - text-align: center; - box-shadow: 0 1px 2px #eee; -} -#tipue_lightbox img -{ - max-width: 200px; - height: auto; -} -#tipue_lightbox_content_title -{ - font: 300 14px/1.7 lato, sans-serif; - color: #111; - padding: 17px 25px 0 25px; - width: 200px; -} -#tipue_lightbox_content_link -{ - float: left; - width: 30px; - height: 30px; - margin-top: 17px; - background: #fff url('img/link.png') no-repeat center; -} -#tipue_lightbox_content_expand -{ - float: left; - width: 30px; - height: 30px; - margin: 17px 0 0 3px; - background: #fff url('img/expand.png') no-repeat center; -} -#tipue_lightbox_content_link:hover, #tipue_lightbox_content_expand:hover -{ - background-color: #f3f3f3; -} - -.tipue_search_content_text -{ - font: 300 14px/1.7 lato, sans-serif; - color: #111; - padding: 13px 0; -} -.tipue_search_content_loc -{ - font: 300 14px/1.5 lato, sans-serif; - color: #111; -} -.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: 47px 0 31px 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 10px 8px 10px; - background-color: #f5f5f5; - background: -webkit-linear-gradient(top, #f7f7f7, #f1f1f1); - background: -moz-linear-gradient(top, #f7f7f7, #f1f1f1); - background: -ms-linear-gradient(top, #f7f7f7, #f1f1f1); - background: -o-linear-gradient(top, #f7f7f7, #f1f1f1); - background: linear-gradient(top, #f7f7f7, #f1f1f1); - border: 1px solid #dcdcdc; - border-radius: 2px; - color: #333; - margin-right: 7px; - text-decoration: none; - text-align: center; -} -#tipue_search_foot_boxes li.current -{ - padding: 7px 10px 8px 10px; - background: #fff; - border: 1px solid #dcdcdc; - border-radius: 2px; - color: #333; - margin-right: 7px; - text-align: center; -} -#tipue_search_foot_boxes li a:hover -{ - border: 1px solid #c3c3c3; - box-shadow: 1px 1px 2px #e3e3e3; -} diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py deleted file mode 100644 index d4e4a3a..0000000 --- a/nikola/plugins/task_render_galleries.py +++ /dev/null @@ -1,338 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 hashlib -import os - -Image = None -try: - import Image as _Image - import ExifTags - Image = _Image -except ImportError: - try: - from PIL import Image, ExifTags # NOQA - except ImportError: - pass - - -from nikola.plugin_categories import Task -from nikola import utils - - -class Galleries(Task): - """Copy theme assets into output.""" - - name = str("render_galleries") - dates = {} - - def gen_tasks(self): - """Render image galleries.""" - - 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'], - 'blog_description': self.site.config['BLOG_DESCRIPTION'], - 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'], - 'gallery_path': self.site.config['GALLERY_PATH'] - } - - # FIXME: lots of work is done even when images don't change, - # which should be moved into the task. - - template_name = "gallery.tmpl" - - gallery_list = [] - for root, dirs, files in os.walk(kw['gallery_path']): - gallery_list.append(root) - if not gallery_list: - yield { - 'basename': str('render_galleries'), - 'actions': [], - } - return - - # gallery_path is "gallery/name" - for gallery_path in gallery_list: - # gallery_name is "name" - splitted = gallery_path.split(os.sep)[1:] - if not splitted: - gallery_name = '' - else: - gallery_name = os.path.join(*splitted) - # output_gallery is "output/GALLERY_PATH/name" - output_gallery = os.path.dirname(os.path.join( - kw["output_folder"], self.site.path("gallery", gallery_name, - None))) - output_name = os.path.join(output_gallery, "index.html") - if not os.path.isdir(output_gallery): - yield { - 'basename': str('render_galleries'), - 'name': output_gallery, - 'actions': [(os.makedirs, (output_gallery,))], - 'targets': [output_gallery], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - # image_list contains "gallery/name/image_name.jpg" - image_list = glob.glob(gallery_path + "/*jpg") +\ - glob.glob(gallery_path + "/*JPG") +\ - glob.glob(gallery_path + "/*PNG") +\ - glob.glob(gallery_path + "/*png") - - # Filter ignore images - try: - def add_gallery_path(index): - return "{0}/{1}".format(gallery_path, index) - - exclude_path = os.path.join(gallery_path, "exclude.meta") - try: - f = open(exclude_path, 'r') - excluded_image_name_list = f.read().split() - except IOError: - excluded_image_name_list = [] - - excluded_image_list = list(map(add_gallery_path, - excluded_image_name_list)) - image_set = set(image_list) - set(excluded_image_list) - image_list = list(image_set) - except IOError: - pass - - # List of sub-galleries - folder_list = [x.split(os.sep)[-2] for x in - glob.glob(os.path.join(gallery_path, '*') + os.sep)] - - crumbs = utils.get_crumbs(gallery_path) - - image_list = [x for x in image_list if "thumbnail" not in x] - # Sort by date - image_list.sort(key=lambda a: self.image_date(a)) - image_name_list = [os.path.basename(x) for x in image_list] - thumbs = [] - # Do thumbnails and copy originals - for img, img_name in list(zip(image_list, image_name_list)): - # img is "galleries/name/image_name.jpg" - # img_name is "image_name.jpg" - # fname, ext are "image_name", ".jpg" - fname, ext = os.path.splitext(img_name) - # thumb_path is - # "output/GALLERY_PATH/name/image_name.thumbnail.jpg" - thumb_path = os.path.join(output_gallery, - ".thumbnail".join([fname, ext])) - # thumb_path is "output/GALLERY_PATH/name/image_name.jpg" - orig_dest_path = os.path.join(output_gallery, img_name) - thumbs.append(os.path.basename(thumb_path)) - yield { - 'basename': str('render_galleries'), - 'name': thumb_path, - 'file_dep': [img], - 'targets': [thumb_path], - 'actions': [ - (self.resize_image, - (img, thumb_path, kw['thumbnail_size'])) - ], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - yield { - 'basename': str('render_galleries'), - 'name': orig_dest_path, - 'file_dep': [img], - 'targets': [orig_dest_path], - 'actions': [ - (self.resize_image, - (img, orig_dest_path, kw['max_image_size'])) - ], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - - # Remove excluded images - if excluded_image_name_list: - for img, img_name in zip(excluded_image_list, - excluded_image_name_list): - # img_name is "image_name.jpg" - # fname, ext are "image_name", ".jpg" - fname, ext = os.path.splitext(img_name) - excluded_thumb_dest_path = os.path.join( - output_gallery, ".thumbnail".join([fname, ext])) - excluded_dest_path = os.path.join(output_gallery, img_name) - yield { - 'basename': str('render_galleries_clean'), - 'name': excluded_thumb_dest_path, - 'file_dep': [exclude_path], - #'targets': [excluded_thumb_dest_path], - 'actions': [ - (utils.remove_file, (excluded_thumb_dest_path,)) - ], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - yield { - 'basename': str('render_galleries_clean'), - 'name': excluded_dest_path, - 'file_dep': [exclude_path], - #'targets': [excluded_dest_path], - 'actions': [ - (utils.remove_file, (excluded_dest_path,)) - ], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - - context = {} - context["lang"] = kw["default_lang"] - context["title"] = os.path.basename(gallery_path) - context["description"] = kw["blog_description"] - if kw['use_filename_as_title']: - img_titles = ['id="{0}" alt="{1}" title="{2}"'.format( - fn[:-4], fn[:-4], utils.unslugify(fn[:-4])) for fn - in image_name_list] - else: - img_titles = [''] * len(image_name_list) - context["images"] = list(zip(image_name_list, thumbs, img_titles)) - context["folders"] = folder_list - context["crumbs"] = crumbs - context["permalink"] = self.site.link( - "gallery", gallery_name, None) - context["enable_comments"] = ( - self.site.config["COMMENTS_IN_GALLERIES"]) - - # Use galleries/name/index.txt to generate a blurb for - # the gallery, if it exists - index_path = os.path.join(gallery_path, "index.txt") - cache_dir = os.path.join(kw["cache_folder"], 'galleries') - if not os.path.isdir(cache_dir): - os.makedirs(cache_dir) - index_dst_path = os.path.join( - cache_dir, - str(hashlib.sha224(index_path.encode('utf-8')).hexdigest() + - '.html')) - if os.path.exists(index_path): - compile_html = self.site.get_compiler(index_path) - yield { - 'basename': str('render_galleries'), - 'name': index_dst_path, - 'file_dep': [index_path], - 'targets': [index_dst_path], - 'actions': [(compile_html, [index_path, index_dst_path])], - 'clean': True, - 'uptodate': [utils.config_changed(kw)], - } - - file_dep = self.site.template_system.template_deps( - template_name) + image_list - - def render_gallery(output_name, context, index_dst_path): - if os.path.exists(index_dst_path): - with codecs.open(index_dst_path, "rb", "utf8") as fd: - context['text'] = fd.read() - file_dep.append(index_dst_path) - else: - context['text'] = '' - self.site.render_template(template_name, output_name, context) - - yield { - 'basename': str('render_galleries'), - 'name': output_name, - 'file_dep': file_dep, - 'targets': [output_name], - 'actions': [(render_gallery, (output_name, context, - index_dst_path))], - 'clean': True, - 'uptodate': [utils.config_changed({ - 1: kw, - 2: self.site.config['GLOBAL_CONTEXT'], - 3: self.site.config["COMMENTS_IN_GALLERIES"], - })], - } - - 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 - 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) - except Exception: - # TODO: inform the user, but do not fail - pass - else: - im.save(dst) - - else: - 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_render_posts.py b/nikola/plugins/task_render_posts.py deleted file mode 100644 index 4be68bf..0000000 --- a/nikola/plugins/task_render_posts.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 codecs -import string - -from nikola.plugin_categories import Task -from nikola import utils, rc4 - - -def wrap_encrypt(path, password): - """Wrap a post with encryption.""" - with codecs.open(path, 'rb+', 'utf8') as inf: - data = inf.read() + "<!--tail-->" - data = CRYPT.substitute(data=rc4.rc4(password, data)) - with codecs.open(path, 'wb+', 'utf8') as outf: - outf.write(data) - - -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'], - } - - flag = False - for lang in kw["translations"]: - deps_dict = copy(kw) - deps_dict.pop('timeline') - for post in kw['timeline']: - source = post.source_path - dest = post.base_path - if not post.is_translation_available(lang) and kw["hide_untranslated_posts"]: - continue - else: - source = post.translated_source_path(lang) - if lang != post.default_lang: - dest = dest + '.' + lang - flag = True - task = { - 'basename': self.name, - 'name': dest, - 'file_dep': post.fragment_deps(lang), - 'targets': [dest], - 'actions': [(self.site.get_compiler(post.source_path), - [source, dest])], - 'clean': True, - 'uptodate': [utils.config_changed(deps_dict)], - } - if post.meta('password'): - task['actions'].append((wrap_encrypt, (dest, post.meta('password')))) - yield task - if flag is False: # Return a dummy task - yield { - 'basename': self.name, - 'name': 'None', - 'uptodate': [True], - 'actions': [], - } - - -CRYPT = string.Template("""\ -<script> -function rc4(key, str) { - var s = [], j = 0, x, res = ''; - for (var i = 0; i < 256; i++) { - s[i] = i; - } - for (i = 0; i < 256; i++) { - j = (j + s[i] + key.charCodeAt(i % key.length)) % 256; - x = s[i]; - s[i] = s[j]; - s[j] = x; - } - i = 0; - j = 0; - for (var y = 0; y < str.length; y++) { - i = (i + 1) % 256; - j = (j + s[i]) % 256; - x = s[i]; - s[i] = s[j]; - s[j] = x; - res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]); - } - return res; -} -function decrypt() { - key = $$("#key").val(); - crypt_div = $$("#encr") - crypted = crypt_div.html(); - decrypted = rc4(key, window.atob(crypted)); - if (decrypted.substr(decrypted.length - 11) == "<!--tail-->"){ - crypt_div.html(decrypted); - $$("#pwform").hide(); - crypt_div.show(); - } else { alert("Wrong password"); }; -} -</script> - -<div id="encr" style="display: none;">${data}</div> -<div id="pwform"> -<form onsubmit="javascript:decrypt(); return false;" class="form-inline"> -<fieldset> -<legend>This post is password-protected.</legend> -<input type="password" id="key" placeholder="Type password here"> -<button type="submit" class="btn">Show Content</button> -</fieldset> -</form> -</div>""") diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py deleted file mode 100644 index 044e0e3..0000000 --- a/nikola/plugins/task_sitemap/__init__.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -except ImportError: - from urllib.parse import urljoin # 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> - <priority>0.5000</priority> - </url> -""" - -get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0] - - -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"], - "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm']) - } - output_path = kw['output_folder'] - sitemap_path = os.path.join(output_path, "sitemap.xml") - - def sitemap(): - with codecs.open(sitemap_path, 'wb+', 'utf8') as outf: - output = kw['output_folder'] - base_url = kw['base_url'] - mapped_exts = kw['mapped_extensions'] - outf.write(header) - locs = {} - for root, dirs, files in os.walk(output): - path = os.path.relpath(root, output) - path = path.replace(os.sep, '/') + '/' - lastmod = get_lastmod(root) - loc = urljoin(base_url, path) - locs[loc] = url_format.format(loc, lastmod) - for fname in files: - if os.path.splitext(fname)[-1] in mapped_exts: - real_path = os.path.join(root, fname) - path = os.path.relpath(real_path, output) - path = path.replace(os.sep, '/') - lastmod = get_lastmod(real_path) - loc = urljoin(base_url, path) - locs[loc] = url_format.format(loc, lastmod) - - for k in sorted(locs.keys()): - outf.write(locs[k]) - outf.write("</urlset>") - - yield { - "basename": "sitemap", - "name": sitemap_path, - "targets": [sitemap_path], - "actions": [(sitemap,)], - "uptodate": [config_changed(kw)], - "clean": True, - } diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/plugins/template/__init__.py diff --git a/nikola/plugins/template_jinja.plugin b/nikola/plugins/template/jinja.plugin index 01e6d8c..53b0fec 100644 --- a/nikola/plugins/template_jinja.plugin +++ b/nikola/plugins/template/jinja.plugin @@ -1,9 +1,9 @@ [Core] Name = jinja -Module = template_jinja +Module = jinja [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Support for Jinja2 templates. diff --git a/nikola/plugins/template_jinja.py b/nikola/plugins/template/jinja.py index b6d762b..a40a476 100644 --- a/nikola/plugins/template_jinja.py +++ b/nikola/plugins/template/jinja.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -26,12 +28,15 @@ import os import json +from collections import deque try: import jinja2 + from jinja2 import meta except ImportError: jinja2 = None # NOQA from nikola.plugin_categories import TemplateSystem +from nikola.utils import makedirs, req_missing class JinjaTemplates(TemplateSystem): @@ -39,6 +44,7 @@ class JinjaTemplates(TemplateSystem): name = "jinja" lookup = None + dependency_cache = {} def __init__(self): """ initialize Jinja2 wrapper with extended set of filters""" @@ -46,31 +52,51 @@ class JinjaTemplates(TemplateSystem): return self.lookup = jinja2.Environment() self.lookup.filters['tojson'] = json.dumps + self.lookup.globals['enumerate'] = enumerate def set_directories(self, directories, cache_folder): """Createa template lookup.""" if jinja2 is None: - raise Exception('To use this theme you need to install the ' - '"Jinja2" package.') + req_missing(['jinja2'], 'use this theme') self.lookup.loader = jinja2.FileSystemLoader(directories, encoding='utf-8') def render_template(self, template_name, output_name, context): """Render the template into output_name using context.""" if jinja2 is None: - raise Exception('To use this theme you need to install the ' - '"Jinja2" package.') + req_missing(['jinja2'], 'use this theme') template = self.lookup.get_template(template_name) output = template.render(**context) if output_name is not None: - try: - os.makedirs(os.path.dirname(output_name)) - except: - pass + makedirs(os.path.dirname(output_name)) with open(output_name, 'w+') as output: output.write(output.encode('utf8')) return output + def render_template_to_string(self, template, context): + """ Render template to a string using context. """ + + return jinja2.Template(template).render(**context) + def template_deps(self, template_name): - # FIXME: unimplemented - return [] + # Cache the lists of dependencies for each template name. + if self.dependency_cache.get(template_name) is None: + # Use a breadth-first search to find all templates this one + # depends on. + queue = deque([template_name]) + visited_templates = set([template_name]) + deps = [] + while len(queue) > 0: + curr = queue.popleft() + source, filename = self.lookup.loader.get_source(self.lookup, + curr)[:2] + deps.append(filename) + ast = self.lookup.parse(source) + dep_names = meta.find_referenced_templates(ast) + for dep_name in dep_names: + if (dep_name not in visited_templates + and dep_name is not None): + visited_templates.add(dep_name) + queue.append(dep_name) + self.dependency_cache[template_name] = deps + return self.dependency_cache[template_name] diff --git a/nikola/plugins/template_mako.plugin b/nikola/plugins/template/mako.plugin index 3fdc354..71f2c71 100644 --- a/nikola/plugins/template_mako.plugin +++ b/nikola/plugins/template/mako.plugin @@ -1,9 +1,9 @@ [Core] Name = mako -Module = template_mako +Module = mako [Documentation] Author = Roberto Alsina Version = 0.1 -Website = http://nikola.ralsina.com.ar +Website = http://getnikola.com Description = Support for Mako templates. diff --git a/nikola/plugins/template_mako.py b/nikola/plugins/template/mako.py index 6ec6698..ae51cac 100644 --- a/nikola/plugins/template_mako.py +++ b/nikola/plugins/template/mako.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Roberto Alsina y otros. +# -*- 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 @@ -23,14 +25,21 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Mako template handlers""" - +from __future__ import unicode_literals, print_function, absolute_import import os import shutil +import sys +import tempfile from mako import util, lexer from mako.lookup import TemplateLookup +from mako.template import Template +from markupsafe import Markup # It's ok, Mako requires it from nikola.plugin_categories import TemplateSystem +from nikola.utils import makedirs, get_logger, STDERR_HANDLER + +LOGGER = get_logger('mako', STDERR_HANDLER) class MakoTemplates(TemplateSystem): @@ -55,8 +64,16 @@ class MakoTemplates(TemplateSystem): return deps def set_directories(self, directories, cache_folder): - """Createa template lookup.""" + """Create a template lookup.""" cache_dir = os.path.join(cache_folder, '.mako.tmp') + # Workaround for a Mako bug, Issue #825 + if sys.version_info[0] == 2: + try: + os.path.abspath(cache_dir).decode('ascii') + except UnicodeEncodeError: + cache_dir = tempfile.mkdtemp() + LOGGER.warning('Because of a Mako bug, setting cache_dir to {0}'.format(cache_dir)) + if os.path.exists(cache_dir): shutil.rmtree(cache_dir) self.lookup = TemplateLookup( @@ -66,21 +83,23 @@ class MakoTemplates(TemplateSystem): def render_template(self, template_name, output_name, context): """Render the template into output_name using context.""" - + context['striphtml'] = striphtml template = self.lookup.get_template(template_name) data = template.render_unicode(**context) if output_name is not None: - try: - os.makedirs(os.path.dirname(output_name)) - except: - pass + makedirs(os.path.dirname(output_name)) with open(output_name, 'w+') as output: output.write(data) return data + def render_template_to_string(self, template, context): + """ Render template to a string using context. """ + + return Template(template).render(**context) + def template_deps(self, template_name): """Returns filenames which are dependencies for a template.""" - # We can cache here because depedencies should + # We can cache here because dependencies should # not change between runs if self.cache.get(template_name, None) is None: template = self.lookup.get_template(template_name) @@ -90,3 +109,7 @@ class MakoTemplates(TemplateSystem): deps += self.template_deps(fname) self.cache[template_name] = tuple(deps) return list(self.cache[template_name]) + + +def striphtml(text): + return Markup(text).striptags() |
