diff options
Diffstat (limited to 'nikola')
330 files changed, 7451 insertions, 4704 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py index 787ce8e..cf4d2e5 100644 --- a/nikola/__init__.py +++ b/nikola/__init__.py @@ -27,7 +27,7 @@ from __future__ import absolute_import import os -__version__ = "6.4.0" +__version__ = "7.0.1" DEBUG = bool(os.getenv('NIKOLA_DEBUG')) from .nikola import Nikola # NOQA diff --git a/nikola/__main__.py b/nikola/__main__.py index 715f5b3..455926d 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -28,6 +28,10 @@ from __future__ import print_function, unicode_literals from operator import attrgetter import os import shutil +try: + import readline # NOQA +except ImportError: + pass # This is only so raw_input/input does nicer things if it's available import sys import traceback @@ -40,39 +44,45 @@ from doit.cmd_run import Run as DoitRun from doit.cmd_clean import Clean as DoitClean from doit.cmd_auto import Auto as DoitAuto from logbook import NullHandler +from blinker import signal from . import __version__ +from .plugin_categories import Command from .nikola import Nikola -from .utils import _reload, sys_decode, get_root_dir, LOGGER, STRICT_HANDLER - +from .utils import _reload, sys_decode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, ColorfulStderrHandler config = {} -def main(args): +def main(args=None): + colorful = False + if sys.stderr.isatty() and os.name != 'nt': + colorful = True + + ColorfulStderrHandler._colorful = colorful + + if args is None: + args = sys.argv[1:] quiet = False - if len(args) > 0 and args[0] == 'build' and '--strict' in args: + if len(args) > 0 and args[0] == b'build' and b'--strict' in args: LOGGER.notice('Running in strict mode') STRICT_HANDLER.push_application() - if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args: + if len(args) > 0 and args[0] == b'build' and b'-q' in args or b'--quiet' in args: nullhandler = NullHandler() nullhandler.push_application() quiet = True global config - colorful = False - if sys.stderr.isatty(): - colorful = True - try: - import colorama - colorama.init() - except ImportError: - if os.name == 'nt': - colorful = False - - root = get_root_dir() - if root: - os.chdir(root) + # Those commands do not require a `conf.py`. (Issue #1132) + # Moreover, actually having one somewhere in the tree can be bad, putting + # the output of that command (the new site) in an unknown directory that is + # not the current working directory. (does not apply to `version`) + argname = args[0] if len(args) > 0 else None + # FIXME there are import plugins in the repo, so how do we handle this? + if argname and argname not in ['init', 'version'] and not argname.startswith('import_'): + root = get_root_dir() + if root: + os.chdir(root) sys.path.append('') try: @@ -86,18 +96,46 @@ def main(args): sys.exit(1) config = {} - config.update({'__colorful__': colorful}) + invariant = False + + if len(args) > 0 and args[0] == b'build' and b'--invariant' in args: + try: + import freezegun + freeze = freezegun.freeze_time("2014-01-01") + freeze.start() + invariant = True + except ImportError: + req_missing(['freezegun'], 'perform invariant builds') + + if config: + if os.path.exists('plugins') and not os.path.exists('plugins/__init__.py'): + with open('plugins/__init__.py', 'w') as fh: + fh.write('# Plugin modules go here.') + + config['__colorful__'] = colorful + config['__invariant__'] = invariant + config['__quiet__'] = quiet site = Nikola(**config) - return DoitNikola(site, quiet).run(args) + _ = DoitNikola(site, quiet).run(args) + + if site.invariant: + freeze.stop() + return _ class Help(DoitHelp): - """show Nikola usage instead of doit """ + """show Nikola usage.""" @staticmethod def print_usage(cmds): """print nikola "usage" (basic help) instructions""" + # Remove 'run'. Nikola uses 'build', though we support 'run' for + # people used to it (eg. doit users). + # WARNING: 'run' is the vanilla doit command, without support for + # --strict, --invariant and --quiet. + del cmds['run'] + print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com/\n\n") print("Available commands:") for cmd in sorted(cmds.values(), key=attrgetter('name')): @@ -123,6 +161,15 @@ class Build(DoitRun): ) opts.append( { + 'name': 'invariant', + 'long': 'invariant', + 'default': False, + 'type': bool, + 'help': "Generate invariant output (for testing only!).", + } + ) + opts.append( + { 'name': 'quiet', 'long': 'quiet', 'short': 'q', @@ -165,6 +212,7 @@ class NikolaTaskLoader(TaskLoader): else: DOIT_CONFIG = { 'reporter': ExecutedOnlyReporter, + 'outfile': sys.stderr, } DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render'] tasks = generate_tasks( @@ -173,6 +221,7 @@ class NikolaTaskLoader(TaskLoader): latetasks = generate_tasks( 'post_render', self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executes after site is rendered.')) + signal('initialized').send(self.nikola) return tasks + latetasks, DOIT_CONFIG @@ -183,13 +232,14 @@ class DoitNikola(DoitMain): def __init__(self, nikola, quiet=False): self.nikola = nikola + nikola.doit = self self.task_loader = self.TASK_LOADER(nikola, quiet) def get_commands(self): # core doit commands cmds = DoitMain.get_commands(self) # load nikola commands - for name, cmd in self.nikola.commands.items(): + for name, cmd in self.nikola._commands.items(): cmds[name] = cmd return cmds @@ -198,21 +248,36 @@ class DoitNikola(DoitMain): args = self.process_args(cmd_args) args = [sys_decode(arg) for arg in args] - if len(args) == 0 or any(arg in ["--help", '-h'] for arg in args): + if len(args) == 0: cmd_args = ['help'] args = ['help'] - # Hide run because Nikola uses build - sub_cmds.pop('run') - if len(args) == 0 or any(arg in ["--version", '-V'] for arg in args): + + if '--help' in args or '-h' in args: + new_cmd_args = ['help'] + cmd_args + new_args = ['help'] + args + + cmd_args = [] + args = [] + + for arg in new_cmd_args: + if arg not in ('--help', '-h'): + cmd_args.append(arg) + for arg in new_args: + if arg not in ('--help', '-h'): + args.append(arg) + + if any(arg in ("--version", '-V') for arg in args): cmd_args = ['version'] args = ['version'] - if len(args) == 0 or args[0] not in sub_cmds.keys() or \ - args[0] == 'build': - # Check for conf.py before launching run + if args[0] not in sub_cmds.keys(): + LOGGER.error("Unknown command {0}".format(args[0])) + return False + if not isinstance(sub_cmds[args[0]], (Command, Help)): # Is a doit command if not self.nikola.configured: LOGGER.error("This command needs to run inside an " "existing Nikola site.") return False + return super(DoitNikola, self).run(cmd_args) @staticmethod diff --git a/nikola/conf.py.in b/nikola/conf.py.in index b398ac3..2f40b59 100644 --- a/nikola/conf.py.in +++ b/nikola/conf.py.in @@ -4,13 +4,22 @@ from __future__ import unicode_literals import time -#!! This is the configuration of Nikola. !!# -#!! You should edit it to your liking. !!# +# !! This is the configuration of Nikola. !! # +# !! You should edit it to your liking. !! # + + +# ! Some settings can be different in different languages. +# ! A comment stating (translatable) is used to denote those. +# ! There are two ways to specify a translatable setting: +# ! (a) BLOG_TITLE = "My Blog" +# ! (b) BLOG_TITLE = {"en": "My Blog", "es": "Mi Blog"} +# ! Option (a) is used when you don't want that setting translated. +# ! Option (b) is used for settings that are different in different languages. # Data about this site -BLOG_AUTHOR = ${BLOG_AUTHOR} -BLOG_TITLE = ${BLOG_TITLE} +BLOG_AUTHOR = ${BLOG_AUTHOR} # (translatable) +BLOG_TITLE = ${BLOG_TITLE} # (translatable) # This is the main URL for your site. It will be used # in a prominent link SITE_URL = ${SITE_URL} @@ -18,37 +27,13 @@ SITE_URL = ${SITE_URL} # If not set, defaults to SITE_URL # BASE_URL = ${SITE_URL} BLOG_EMAIL = ${BLOG_EMAIL} -BLOG_DESCRIPTION = ${BLOG_DESCRIPTION} +BLOG_DESCRIPTION = ${BLOG_DESCRIPTION} # (translatable) # Nikola is multilingual! # # Currently supported languages are: -# bg Bulgarian -# ca Catalan -# cs Czech [ALTERNATIVELY cz] -# de German -# el Greek [NOT gr!] -# en English -# eo Esperanto -# es Spanish -# et Estonian -# eu Basque -# fa Persian -# fi Finnish -# fr French -# hi Hindi -# hr Croatian -# it Italian -# ja Japanese [NOT jp!] -# nb Norwegian Bokmål -# nl Dutch -# pt_br Portuguese (Brasil) -# pl Polish -# ru Russian -# sl Slovenian [NOT sl_si!] -# tr Turkish (Turkey) [NOT tr_tr!] -# ur Urdu -# zh_cn Chinese (Simplified) +# +${_SUPPORTED_LANGUAGES} # # If you want to use Nikola with a non-supported language you have to provide # a module containing the necessary translations @@ -62,40 +47,48 @@ DEFAULT_LANG = ${DEFAULT_LANG} # What other languages do you have? # The format is {"translationcode" : "path/to/translation" } # the path will be used as a prefix for the generated pages location -TRANSLATIONS = { - DEFAULT_LANG: "", - # Example for another language: - # "es": "./es", -} +TRANSLATIONS = ${TRANSLATIONS} # What will translated input files be named like? -# If you have a page something.rst, then something.rst.pl will be considered +# If you have a page something.rst, then something.pl.rst will be considered # its Polish translation. -# (in the above example: path == "something", lang == "pl", ext == "rst") +# (in the above example: path == "something", ext == "rst", lang == "pl") # this pattern is also used for metadata: -# something.meta -> something.meta.pl +# something.meta -> something.pl.meta TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN} -# If you don't want your Polish files to be considered Perl code, use this: -# TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}" -# Note that this pattern will become the default in v7.0.0. - # Links for the sidebar / navigation bar. # You should provide a key-value pair for each used language. -NAVIGATION_LINKS = { - DEFAULT_LANG: ( - ('/archive.html', 'Archives'), - ('/categories/index.html', 'Tags'), - ('/rss.xml', 'RSS'), - ), -} +# (the same way you would do with a (translatable) setting.) +NAVIGATION_LINKS = ${NAVIGATION_LINKS} + +# Name of the theme to use. +THEME = ${THEME} ############################################## # Below this point, everything is optional ############################################## +# Post's dates are considered in UTC by default, if you want to use +# another time zone, please set TIMEZONE to match. Check the available +# list from Wikipedia: +# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# (eg. 'Europe/Zurich') +# Also, if you want to use a different time zone in some of your posts, +# you can use the ISO 8601/RFC 3339 format (ex. 2012-03-30T23:00:00+02:00) +TIMEZONE = ${TIMEZONE} + +# If you want to use ISO 8601 (also valid RFC 3339) throughout Nikola +# (especially in new_post), set this to True. +# Note that this does not affect DATE_FORMAT. +# FORCE_ISO8601 = False + +# Date format used to display post dates. +# (str used by datetime.datetime.strftime) +# DATE_FORMAT = '%Y-%m-%d %H:%M' + # While nikola can select a sensible locale for each language, # sometimes explicit control can come handy. # In this file we express locales in the string form that @@ -114,7 +107,7 @@ NAVIGATION_LINKS = { # # That fragment could have an associated metadata file (whatever/thing.meta), # and optionally translated files (example for spanish, with code "es"): -# whatever/thing.txt.es and whatever/thing.meta.es +# whatever/thing.es.txt and whatever/thing.es.meta # # This assumes you use the default TRANSLATIONS_PATTERN. # @@ -154,11 +147,21 @@ COMPILERS = ${COMPILERS} # Set to False for two-file posts, with separate metadata. # ONE_FILE_POSTS = True -# If this is set to True, then posts that are not translated to a language -# LANG will not be visible at all in the pages in that language. -# If set to False, the DEFAULT_LANG version will be displayed for +# If this is set to True, the DEFAULT_LANG version will be displayed for # untranslated posts. -# HIDE_UNTRANSLATED_POSTS = False +# If this is set to False, then posts that are not translated to a language +# LANG will not be visible at all in the pages in that language. +# Formerly known as HIDE_UNTRANSLATED_POSTS (inverse) +# SHOW_UNTRANSLATED_POSTS = True + +# Nikola supports logo display. If you have one, you can put the URL here. +# Final output is <img src="LOGO_URL" id="logo" alt="BLOG_TITLE">. +# The URL may be relative to the site root. +# LOGO_URL = '' + +# If you want to hide the title of your website (for example, if your logo +# already contains the text), set this to False. +# SHOW_BLOG_TITLE = True # Paths for different autogenerated bits. These are combined with the # translation paths. @@ -171,7 +174,7 @@ COMPILERS = ${COMPILERS} # If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain # the posts themselves. If set to False, it will be just a list of links. -# TAG_PAGES_ARE_INDEXES = True +# TAG_PAGES_ARE_INDEXES = False # Final location for the main blog page and sibling paginated pages is # output / TRANSLATION[lang] / INDEX_PATH / index-*.html @@ -212,7 +215,7 @@ COMPILERS = ${COMPILERS} # relative URL. # # If you don't need any of these, just set to [] -# REDIRECTIONS = ${REDIRECTIONS} +REDIRECTIONS = ${REDIRECTIONS} # Commands to execute to deploy. Can be anything, for example, # you may use rsync: @@ -222,6 +225,14 @@ COMPILERS = ${COMPILERS} # To do manual deployment, set it to [] # DEPLOY_COMMANDS = [] +# For user.github.io/organization.github.io pages, the DEPLOY branch +# MUST be 'master', and 'gh-pages' for other repositories. +# GITHUB_SOURCE_BRANCH = 'master' +# GITHUB_DEPLOY_BRANCH = 'gh-pages' + +# The name of the remote where you wish to push to, using github_deploy. +# GITHUB_REMOTE_NAME = 'origin' + # Where the output site should be located # If you don't use an absolute path, it will be considered as relative # to the location of conf.py @@ -308,10 +319,7 @@ COMPILERS = ${COMPILERS} # INDEXES_TITLE = "" # If this is empty, defaults to BLOG_TITLE # INDEXES_PAGES = "" # If this is empty, defaults to '[old posts,] page %d' (see above) # INDEXES_PAGES_MAIN = False # If True, INDEXES_PAGES is also displayed on - # the main (the newest) index page (index.html) - -# Name of the theme to use. -THEME = ${THEME} +# # the main (the newest) index page (index.html) # Color scheme to be used for code blocks. If your theme provides # "assets/css/code.css" this is ignored. @@ -328,15 +336,9 @@ THEME = ${THEME} # THEME_REVEAL_CONFIG_TRANSITION = 'cube' # You can also use: page/concave/linear/none/default -# date format used to display post dates. -# (str used by datetime.datetime.strftime) -# DATE_FORMAT = '%Y-%m-%d %H:%M' - # FAVICONS contains (name, file, size) tuples. # Used for create favicon link like this: # <link rel="name" href="file" sizes="size"/> -# For creating favicons, take a look at: -# http://www.netmagazine.com/features/create-perfect-favicon # FAVICONS = { # ("icon", "/favicon.ico", "16x16"), # ("icon", "/icon_128x128.png", "128x128"), @@ -345,15 +347,25 @@ THEME = ${THEME} # Show only teasers in the index pages? Defaults to False. # INDEX_TEASERS = False -# A HTML fragment with the Read more... link. +# HTML fragments with the Read more... links. # The following tags exist and are replaced for you: -# {link} A link to the full post page. -# {read_more} The string “Read more” in the current language. -# {{ A literal { (U+007B LEFT CURLY BRACKET) -# }} A literal } (U+007D RIGHT CURLY BRACKET) -# READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>' +# {link} A link to the full post page. +# {read_more} The string “Read more” in the current language. +# {reading_time} An estimate of how long it will take to read the post. +# {remaining_reading_time} An estimate of how long it will take to read the post, sans the teaser. +# {min_remaining_read} The string “{remaining_reading_time} min remaining to read” in the current language. +# {paragraph_count} The amount of paragraphs in the post. +# {remaining_paragraph_count} The amount of paragraphs in the post, sans the teaser. +# {{ A literal { (U+007B LEFT CURLY BRACKET) +# }} A literal } (U+007D RIGHT CURLY BRACKET) + +# 'Read more...' for the index page, if INDEX_TEASERS is True (translatable) +INDEX_READ_MORE_LINK = ${INDEX_READ_MORE_LINK} +# 'Read more...' for the RSS_FEED, if RSS_TEASERS is True (translatable) +RSS_READ_MORE_LINK = ${RSS_READ_MORE_LINK} # A HTML fragment describing the license, for the sidebar. +# (translatable) LICENSE = "" # I recommend using the Creative Commons' wizard: # http://creativecommons.org/choose/ @@ -364,24 +376,45 @@ LICENSE = "" # src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" # A small copyright notice for the page footer (in HTML). +# (translatable) CONTENT_FOOTER = 'Contents © {date} \ <a href="mailto:{email}">{author}</a> - Powered by \ <a href="http://getnikola.com" rel="nofollow">Nikola</a> \ {license}' -CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, - author=BLOG_AUTHOR, - date=time.gmtime().tm_year, - license=LICENSE) + +# Things that will be passed to CONTENT_FOOTER.format(). This is done +# for translatability, as dicts are not formattable. Nikola will +# intelligently format the setting properly. +# The setting takes a dict. The keys are languages. The values are +# tuples of tuples of positional arguments and dicts of keyword arguments +# to format(). For example, {'en': (('Hello'), {'target': 'World'})} +# results in CONTENT_FOOTER['en'].format('Hello', target='World'). +# WARNING: If you do not use multiple languages with CONTENT_FOOTER, this +# still needs to be a dict of this format. (it can be empty if you +# do not need formatting) +# (translatable) +CONTENT_FOOTER_FORMATS = { + DEFAULT_LANG: ( + (), + { + "email": BLOG_EMAIL, + "author": BLOG_AUTHOR, + "date": time.gmtime().tm_year, + "license": LICENSE + } + ) +} # To use comments, you can choose between different third party comment -# systems, one of "disqus", "livefyre", "intensedebate", "moot", -# "googleplus", "facebook" or "isso" -# COMMENT_SYSTEM = ${COMMENT_SYSTEM} +# systems. The following comment systems are supported by Nikola: +${_SUPPORTED_COMMENT_SYSTEMS} +# You can leave this option blank to disable comments. +COMMENT_SYSTEM = ${COMMENT_SYSTEM} # And you also need to add your COMMENT_SYSTEM_ID which # depends on what comment system you use. The default is # "nikolademo" which is a test account for Disqus. More information # is in the manual. -# COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} +COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} # Enable annotations using annotateit.org? # If set to False, you can still enable them for individual posts and pages @@ -418,6 +451,12 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # if /2012 includes any files (including index.html)... add it to the sitemap # SITEMAP_INCLUDE_FILELESS_DIRS = True +# List of files relative to the server root (!) that will be asked to be excluded +# from indexing and other robotic spidering. * is supported. Will only be effective +# if SITE_URL points to server root. The list is used to exclude resources from +# /robots.txt and /sitemap.xml, and to inform search engines about /sitemapindex.xml. +# ROBOTS_EXCLUSIONS = ["/archive.html", "/category/*.html"] + # Instead of putting files in <slug>.html, put them in # <slug>/index.html. Also enables STRIP_INDEXES # This can be disabled on a per-page/post basis by adding @@ -441,27 +480,25 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # SCHEDULE_RULE = '' # If True, use the scheduling rule to all posts by default # SCHEDULE_ALL = False -# If True, schedules post to today if possible, even if scheduled hour is over -# SCHEDULE_FORCE_TODAY = False # Do you want a add a Mathjax config file? # MATHJAX_CONFIG = "" # If you are using the compile-ipynb plugin, just add this one: -#MATHJAX_CONFIG = """ -#<script type="text/x-mathjax-config"> -#MathJax.Hub.Config({ -# tex2jax: { -# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ], -# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ] -# }, -# displayAlign: 'left', // Change this to 'center' to center equations. -# "HTML-CSS": { -# styles: {'.MathJax_Display': {"margin": 0}} -# } -#}); -#</script> -#""" +# MATHJAX_CONFIG = """ +# <script type="text/x-mathjax-config"> +# MathJax.Hub.Config({ +# tex2jax: { +# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ], +# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ] +# }, +# displayAlign: 'left', // Change this to 'center' to center equations. +# "HTML-CSS": { +# styles: {'.MathJax_Display': {"margin": 0}} +# } +# }); +# </script> +# """ # Do you want to customize the nbconversion of your IPython notebook? # IPYNB_CONFIG = {} @@ -469,13 +506,16 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # called `toggle.tpl` which has to be located in your site/blog main folder: # IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}} -# What MarkDown extensions to enable? +# What Markdown extensions to enable? # You will also get gist, nikola and podcast because those are # done in the code, hope you don't mind ;-) +# Note: most Nikola-specific extensions are done via the Nikola plugin system, +# with the MarkdownExtension class and should not be added here. # MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite'] # Social buttons. This is sample code for AddThis (which was the default for a # long time). Insert anything you want here, or even make it empty. +# (translatable) # SOCIAL_BUTTONS_CODE = """ # <!-- Social buttons --> # <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style"> @@ -486,20 +526,25 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # <li><a class="addthis_button_twitter"></a> # </ul> # </div> -# <script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> +# <script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> # <!-- End of social buttons --> # """ -# Hide link to source for the posts? -# HIDE_SOURCELINK = False +# Show link to source for the posts? +# Formerly known as HIDE_SOURCELINK (inverse) +# SHOW_SOURCELINK = True # Copy the source files for your pages? -# Setting it to False implies HIDE_SOURCELINK = True +# Setting it to False implies SHOW_SOURCELINK = False # COPY_SOURCES = True # Modify the number of Post per Index Page # Defaults to 10 # INDEX_DISPLAY_POST_COUNT = 10 +# By default, Nikola generates RSS files for the website and for tags, and +# links to it. Set this to False to disable everything RSS-related. +# GENERATE_RSS = True + # RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, # the base.tmpl will use the feed Nikola generates. However, you may want to # change it for a feedburner feed or something else. @@ -508,83 +553,59 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # Show only teasers in the RSS feed? Default to True # RSS_TEASERS = True +# Strip HTML in the RSS feed? Default to False +# RSS_PLAIN = False + # A search form to search this site, for the sidebar. You can use a google # custom search (http://www.google.com/cse/) # Or a duckduckgo search: https://duckduckgo.com/search_box.html # Default is no search form. +# (translatable) # SEARCH_FORM = "" # # This search form works for any site and looks good in the "site" theme where # it appears on the navigation bar: # -#SEARCH_FORM = """ -#<!-- Custom search --> -#<form method="get" id="search" action="http://duckduckgo.com/" -# class="navbar-form pull-left"> -#<input type="hidden" name="sites" value="%s"/> -#<input type="hidden" name="k8" value="#444444"/> -#<input type="hidden" name="k9" value="#D51920"/> -#<input type="hidden" name="kt" value="h"/> -#<input type="text" name="q" maxlength="255" -# placeholder="Search…" class="span2" style="margin-top: 4px;"/> -#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> -#</form> -#<!-- End of custom search --> -#""" % SITE_URL +# SEARCH_FORM = """ +# <!-- Custom search --> +# <form method="get" id="search" action="//duckduckgo.com/" +# class="navbar-form pull-left"> +# <input type="hidden" name="sites" value="%s"/> +# <input type="hidden" name="k8" value="#444444"/> +# <input type="hidden" name="k9" value="#D51920"/> +# <input type="hidden" name="kt" value="h"/> +# <input type="text" name="q" maxlength="255" +# placeholder="Search…" class="span2" style="margin-top: 4px;"/> +# <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> +# </form> +# <!-- End of custom search --> +# """ % SITE_URL # # If you prefer a google search form, here's an example that should just work: -#SEARCH_FORM = """ -#<!-- Custom search with google--> -#<form id="search" action="http://google.com/search" method="get" class="navbar-form pull-left"> -#<input type="hidden" name="q" value="site:%s" /> -#<input type="text" name="q" maxlength="255" results="0" placeholder="Search"/> -#</form> -#<!-- End of custom search --> -#""" % SITE_URL - -# Also, there is a local search plugin you can use, based on Tipue, but it requires setting several -# options: - # SEARCH_FORM = """ -# <span class="navbar-form pull-left"> -# <input type="text" id="tipue_search_input"> -# </span>""" -# -# BODY_END = """ -# <script type="text/javascript" src="/assets/js/tipuesearch_set.js"></script> -# <script type="text/javascript" src="/assets/js/tipuesearch.js"></script> -# <script type="text/javascript"> -# $(document).ready(function() { - # $('#tipue_search_input').tipuesearch({ - # 'mode': 'json', - # 'contentLocation': '/assets/js/tipuesearch_content.json', - # 'showUrl': false - # }); -# }); -# </script> -# """ - -# EXTRA_HEAD_DATA = """ -# <link rel="stylesheet" type="text/css" href="/assets/css/tipuesearch.css"> -# <div id="tipue_search_content" style="margin-left: auto; margin-right: auto; padding: 20px;"></div> -# """ -# ENABLED_EXTRAS = ['local_search'] -# -###### End of local search example - +# <!-- Custom search with google--> +# <form id="search" action="//www.google.com/search" method="get" class="navbar-form pull-left"> +# <input type="hidden" name="q" value="site:%s" /> +# <input type="text" name="q" maxlength="255" results="0" placeholder="Search"/> +# </form> +# <!-- End of custom search --> +#""" % SITE_URL -# Use content distribution networks for jquery and twitter-bootstrap css and js -# If this is True, jquery is served from the Google CDN and twitter-bootstrap -# is served from the NetDNA CDN +# Use content distribution networks for jquery, twitter-bootstrap css and js, +# and html5shiv (for older versions of Internet Explorer) +# If this is True, jquery and html5shiv is served from the Google and twitter- +# bootstrap is served from the NetDNA CDN # Set this to False if you want to host your site without requiring access to # external resources. # USE_CDN = False # Extra things you want in the pages HEAD tag. This will be added right # before </head> +# (translatable) # EXTRA_HEAD_DATA = "" # Google Analytics or whatever else you use. Added to the bottom of <body> # in the default template (base.tmpl). +# (translatable) # BODY_END = "" # The possibility to extract metadata from the filename by using a @@ -602,12 +623,21 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md' # FILE_METADATA_REGEXP = None +# If you hate "Filenames with Capital Letters and Spaces.md", you should +# set this to true. +UNSLUGIFY_TITLES = True + # Additional metadata that is added to a post when creating a new_post # ADDITIONAL_METADATA = {} -# Nikola supports Twitter Card summaries / Open Graph. -# Twitter cards make it possible for you to attach media to Tweets -# that link to your content. +# Nikola supports Open Graph Protocol data for enhancing link sharing and +# discoverability of your site on Facebook, Google+, and other services. +# Open Graph is enabled by default. +# USE_OPEN_GRAPH = True + +# Nikola supports Twitter Card summaries +# Twitter cards are disabled by default. They make it possible for you to +# attach media to Tweets that link to your content. # # IMPORTANT: # Please note, that you need to opt-in for using Twitter Cards! @@ -619,7 +649,7 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # over the cleartext username. Specifying an ID is not necessary. # Displaying images is currently not supported. # TWITTER_CARD = { -# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph +# # 'use_twitter_cards': True, # enable Twitter Cards # # 'site': '@website', # twitter nick for the website # # 'site:id': 123456, # Same as site, but the website's Twitter user ID # # instead. @@ -627,17 +657,6 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # # 'creator:id': 654321, # Same as creator, but the Twitter user's ID. # } - -# Post's dates are considered in UTC by default, if you want to use -# another time zone, please set TIMEZONE to match. Check the available -# list from Wikipedia: -# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones -# (eg. 'Europe/Zurich') -# Also, if you want to use a different time zone in some of your posts, -# you can use W3C-DTF Format (ex. 2012-03-30T23:00:00+02:00) -# -# TIMEZONE = 'UTC' - # If webassets is installed, bundle JS and CSS to make site loading faster # USE_BUNDLES = True @@ -649,16 +668,6 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # repository. # EXTRA_PLUGINS_DIRS = [] -# Experimental plugins - use at your own risk. -# They probably need some manual adjustments - please see their respective -# readme. -# ENABLED_EXTRAS = [ -# 'planetoid', -# 'ipynb', -# 'local_search', -# 'render_mustache', -# ] - # List of regular expressions, links matching them will always be considered # valid by "nikola check -l" # LINK_CHECK_WHITELIST = [] @@ -675,18 +684,23 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # DEMOTE_HEADERS = 1 # You can configure the logging handlers installed as plugins or change the -# log level of the default stdout handler. +# log level of the default stderr handler. +# WARNING: The stderr handler allows only the loglevels of 'INFO' and 'DEBUG'. +# This is done for safety reasons, as blocking out anything other +# than 'DEBUG' may hide important information and break the user +# experience! + LOGGING_HANDLERS = { - 'stderr': {'loglevel': 'WARNING', 'bubble': True}, - #'smtp': { - # 'from_addr': 'test-errors@example.com', - # 'recipients': ('test@example.com'), - # 'credentials':('testusername', 'password'), - # 'server_addr': ('127.0.0.1', 25), - # 'secure': (), - # 'level': 'DEBUG', - # 'bubble': True - #} + 'stderr': {'loglevel': 'INFO', 'bubble': True}, + # 'smtp': { + # 'from_addr': 'test-errors@example.com', + # 'recipients': ('test@example.com'), + # 'credentials':('testusername', 'password'), + # 'server_addr': ('127.0.0.1', 25), + # 'secure': (), + # 'level': 'DEBUG', + # 'bubble': True + # } } # Templates will use those filters, along with the defaults. diff --git a/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg Binary files differindex 8be0531..43ea5db 100644 --- a/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg Binary files differindex e350491..9274950 100644 --- a/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg Binary files differindex 7549d09..f47d2ae 100644 --- a/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg Binary files differindex 7e4a6a0..3c12b0e 100644 --- a/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg Binary files differindex 730b4de..8355d86 100644 --- a/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg Binary files differindex 1b9edcb..7d8b95b 100644 --- a/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg +++ b/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg diff --git a/nikola/data/samplesite/posts/1.rst b/nikola/data/samplesite/posts/1.rst index 25e56f8..7116a7a 100644 --- a/nikola/data/samplesite/posts/1.rst +++ b/nikola/data/samplesite/posts/1.rst @@ -1,7 +1,8 @@ .. title: Welcome to Nikola .. slug: welcome-to-nikola -.. date: 2012/03/30 23:00 +.. date: 2012-03-30 23:00:00 UTC-03:00 .. tags: nikola, python, demo, blog +.. author: Roberto Alsina .. link: http://getnikola.com .. description: diff --git a/nikola/data/samplesite/stories/1.rst b/nikola/data/samplesite/stories/1.rst index 27c75d8..b662fae 100644 --- a/nikola/data/samplesite/stories/1.rst +++ b/nikola/data/samplesite/stories/1.rst @@ -1,6 +1,6 @@ .. title: Nikola: it generates static .. slug: about-nikola -.. date: 2012/03/30 23:00 +.. date: 2012-03-30 23:00:00 UTC-03:00 .. tags: .. link: .. description: diff --git a/nikola/data/samplesite/stories/a-study-in-scarlet.txt b/nikola/data/samplesite/stories/a-study-in-scarlet.txt index 10f9528..2dfee52 100644 --- a/nikola/data/samplesite/stories/a-study-in-scarlet.txt +++ b/nikola/data/samplesite/stories/a-study-in-scarlet.txt @@ -1,7 +1,7 @@ .. link: .. description: .. tags: -.. date: 2013/08/27 18:20:55 +.. date: 2013-08-27 18:20:55 UTC-03:00 .. title: A STUDY IN SCARLET. .. slug: a-study-in-scarlet diff --git a/nikola/data/samplesite/stories/bootstrap-demo.rst b/nikola/data/samplesite/stories/bootstrap-demo.rst index 520e4b0..a7be1a9 100644 --- a/nikola/data/samplesite/stories/bootstrap-demo.rst +++ b/nikola/data/samplesite/stories/bootstrap-demo.rst @@ -1,6 +1,6 @@ .. title: Bootstrap Demo .. slug: bootstrap-demo -.. date: 2012/03/30 23:00 +.. date: 2012-03-30 23:00:00 UTC-03:00 .. tags: bootstrap, demo .. link: http://getnikola.com .. description: diff --git a/nikola/data/samplesite/stories/charts.txt b/nikola/data/samplesite/stories/charts.txt index 2c90fdf..72fedb1 100644 --- a/nikola/data/samplesite/stories/charts.txt +++ b/nikola/data/samplesite/stories/charts.txt @@ -1,7 +1,7 @@ .. link: .. description: .. tags: -.. date: 2013/08/27 18:20:55 +.. date: 2013-08-27 18:20:55 UTC-03:00 .. title: Charts .. slug: charts diff --git a/nikola/data/samplesite/stories/listings-demo.rst b/nikola/data/samplesite/stories/listings-demo.rst index 7875f17..3bb8dc6 100644 --- a/nikola/data/samplesite/stories/listings-demo.rst +++ b/nikola/data/samplesite/stories/listings-demo.rst @@ -1,6 +1,6 @@ .. title: Listings Demo .. slug: listings-demo -.. date: 2012/12/15 10:16:20 +.. date: 2012-12-15 10:16:20 UTC-03:00 .. tags: .. link: .. description: diff --git a/nikola/data/samplesite/stories/quickref.rst b/nikola/data/samplesite/stories/quickref.rst index 52e786f..7886cd1 100644 --- a/nikola/data/samplesite/stories/quickref.rst +++ b/nikola/data/samplesite/stories/quickref.rst @@ -1,6 +1,6 @@ .. title: A reStructuredText Reference .. slug: quickref -.. date: 2012/03/30 23:00 +.. date: 2012-03-30 23:00:00 UTC-03:00 .. tags: .. link: .. description: diff --git a/nikola/data/samplesite/stories/quickstart.rst b/nikola/data/samplesite/stories/quickstart.rst index 4282b23..5b78807 100644 --- a/nikola/data/samplesite/stories/quickstart.rst +++ b/nikola/data/samplesite/stories/quickstart.rst @@ -1,6 +1,6 @@ .. title: A reStructuredText Primer .. slug: quickstart -.. date: 2012/03/30 23:00 +.. date: 2012-03-30 23:00:00 UTC-03:00 .. tags: .. link: .. description: diff --git a/nikola/data/samplesite/stories/slides-demo.rst b/nikola/data/samplesite/stories/slides-demo.rst index fb1356b..0d07bbc 100644 --- a/nikola/data/samplesite/stories/slides-demo.rst +++ b/nikola/data/samplesite/stories/slides-demo.rst @@ -1,6 +1,6 @@ .. title: Slides Demo .. slug: slides-demo -.. date: 2012/12/27 10:16:20 +.. date: 2012-12-27 10:16:20 UTC-03:00 .. tags: .. link: .. description: diff --git a/nikola/data/symlinked.txt b/nikola/data/symlinked.txt new file mode 100644 index 0000000..5a08781 --- /dev/null +++ b/nikola/data/symlinked.txt @@ -0,0 +1,151 @@ +docs/sphinx/creating-a-site.txt +docs/sphinx/creating-a-theme.txt +docs/sphinx/extending.txt +docs/sphinx/internals.txt +docs/sphinx/manual.txt +docs/sphinx/social_buttons.txt +docs/sphinx/theming.txt +docs/sphinx/upgrading-to-v6.txt +nikola/data/samplesite/stories/creating-a-theme.rst +nikola/data/samplesite/stories/extending.txt +nikola/data/samplesite/stories/internals.txt +nikola/data/samplesite/stories/manual.rst +nikola/data/samplesite/stories/social_buttons.txt +nikola/data/samplesite/stories/theming.rst +nikola/data/samplesite/stories/upgrading-to-v6.txt +nikola/data/themes/base/messages/messages_cz.py +nikola/data/themes/bootstrap-jinja/assets/css/bootstrap-responsive.css +nikola/data/themes/bootstrap-jinja/assets/css/bootstrap-responsive.min.css +nikola/data/themes/bootstrap-jinja/assets/css/bootstrap.css +nikola/data/themes/bootstrap-jinja/assets/css/bootstrap.min.css +nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css +nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png +nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif +nikola/data/themes/bootstrap-jinja/assets/css/theme.css +nikola/data/themes/bootstrap-jinja/assets/img/glyphicons-halflings-white.png +nikola/data/themes/bootstrap-jinja/assets/img/glyphicons-halflings.png +nikola/data/themes/bootstrap-jinja/assets/js/bootstrap.js +nikola/data/themes/bootstrap-jinja/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js +nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox-min.js +nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js +nikola/data/themes/bootstrap-jinja/assets/js/jquery.min.js +nikola/data/themes/bootstrap-jinja/assets/js/jquery.min.map +nikola/data/themes/bootstrap-jinja/bundles +nikola/data/themes/bootstrap/assets/css/colorbox.css +nikola/data/themes/bootstrap/assets/css/images/controls.png +nikola/data/themes/bootstrap/assets/css/images/loading.gif +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +nikola/data/themes/bootstrap/assets/js/jquery.colorbox-min.js +nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js +nikola/data/themes/bootstrap/assets/js/jquery.min.js +nikola/data/themes/bootstrap/assets/js/jquery.min.map +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map +nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css +nikola/data/themes/bootstrap3-jinja/assets/css/docs.css +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png +nikola/data/themes/bootstrap3-jinja/assets/css/rst.css +nikola/data/themes/bootstrap3-jinja/assets/css/theme.css +nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot +nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg +nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf +nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff +nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js +nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap3-jinja/bundles +nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css +nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map +nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css +nikola/data/themes/bootstrap3/assets/css/bootstrap.css +nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map +nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css +nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot +nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg +nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf +nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff +nikola/data/themes/bootstrap3/assets/js/bootstrap.js +nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js diff --git a/nikola/data/themes/base-jinja/AUTHORS.txt b/nikola/data/themes/base-jinja/AUTHORS.txt new file mode 100644 index 0000000..043d497 --- /dev/null +++ b/nikola/data/themes/base-jinja/AUTHORS.txt @@ -0,0 +1 @@ +Roberto Alsina <https://github.com/ralsina> diff --git a/nikola/data/themes/base-jinja/README.md b/nikola/data/themes/base-jinja/README.md new file mode 100644 index 0000000..5d1da94 --- /dev/null +++ b/nikola/data/themes/base-jinja/README.md @@ -0,0 +1,4 @@ +This theme has almost no styling, it's meant as a basis from which other +themes can be developed. + +Therefore, most "advanced" features, such as slides or galleries, are broken. diff --git a/nikola/data/themes/base-jinja/bundles b/nikola/data/themes/base-jinja/bundles new file mode 100644 index 0000000..4760181 --- /dev/null +++ b/nikola/data/themes/base-jinja/bundles @@ -0,0 +1,2 @@ +assets/css/all.css=rst.css,code.css,theme.css +assets/css/all-nocdn.css=rst.css,code.css,theme.css diff --git a/nikola/data/themes/base-jinja/engine b/nikola/data/themes/base-jinja/engine new file mode 100644 index 0000000..6f04b30 --- /dev/null +++ b/nikola/data/themes/base-jinja/engine @@ -0,0 +1 @@ +jinja diff --git a/nikola/data/themes/base-jinja/parent b/nikola/data/themes/base-jinja/parent new file mode 100644 index 0000000..df967b9 --- /dev/null +++ b/nikola/data/themes/base-jinja/parent @@ -0,0 +1 @@ +base diff --git a/nikola/data/themes/base-jinja/templates/annotation_helper.tmpl b/nikola/data/themes/base-jinja/templates/annotation_helper.tmpl new file mode 100644 index 0000000..86d09b2 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/annotation_helper.tmpl @@ -0,0 +1,16 @@ +{% macro css() %} + <link rel="stylesheet" href="http://assets.annotateit.org/annotator/v1.2.5/annotator.min.css"> +{% endmacro %} + +{% macro code() %} + <script src="http://code.jquery.com/jquery-migrate-1.2.1.js"></script> + <script src="http://assets.annotateit.org/annotator/v1.2.7/annotator-full.js"></script> + <script> + jQuery(function ($) { + $('body').annotator().annotator('setupPlugins', {}, { + // Disable filter bar + Filter: false + }); + }); + </script> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/base.tmpl b/nikola/data/themes/base-jinja/templates/base.tmpl new file mode 100644 index 0000000..2b15177 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/base.tmpl @@ -0,0 +1,25 @@ +{# -*- coding: utf-8 -*- #} +{% import 'base_helper.tmpl' as base with context %} +{% import 'base_header.tmpl' as header with context %} +{% import 'base_footer.tmpl' as footer with context %} +{% import 'annotation_helper.tmpl' as annotations with context %} +{{ set_locale(lang) }} +{{ base.html_headstart() }} +{% block extra_head %} +{# Leave this block alone. #} +{% endblock %} +{{ template_hooks['extra_head']() }} +</head> +<body> + <div id="container"> + {{ header.html_header() }} + <main id="content"> + {% block content %}{% endblock %} + </main> + {{ footer.html_footer() }} + </div> + {{ body_end }} + {{ template_hooks['body_end']() }} + {{ base.late_load_js() }} +</body> +</html> diff --git a/nikola/data/themes/base-jinja/templates/base_footer.tmpl b/nikola/data/themes/base-jinja/templates/base_footer.tmpl new file mode 100644 index 0000000..7fcf616 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/base_footer.tmpl @@ -0,0 +1,11 @@ +{# -*- coding: utf-8 -*- #} +{% import 'base_helper.tmpl' as base with context %} + +{% macro html_footer() %} + {% if content_footer %} + <footer id="footer" role="contentinfo"> + <p>{{ content_footer }}</p> + {{ template_hooks['page_footer']() }} + </footer> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/base_header.tmpl b/nikola/data/themes/base-jinja/templates/base_header.tmpl new file mode 100644 index 0000000..1001db3 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/base_header.tmpl @@ -0,0 +1,66 @@ +{# -*- coding: utf-8 -*- #} +{% import 'base_helper.tmpl' as base with context %} + +{% macro html_header() %} + <header id="header" role="banner"> + {{ html_site_title() }} + {{ html_translation_header() }} + {{ html_navigation_links() }} + {% if search_form %} + <div class="searchform" role="search"> + {{ search_form }} + </div> + {% endif %} + </header> + {{ template_hooks['page_header']() }} +{% endmacro %} + +{% macro html_site_title() %} + <h1 id="brand"><a href="{{ abs_link('/') }}" title="{{ blog_title }}" rel="home"> + {% if logo_url %} + <img src="{{ logo_url }}" alt="{{ blog_title }}" id="logo"> + {% endif %} + + {% if show_blog_title %} + <span id="blog-title">{{ blog_title }}</span> + {% endif %} + </a></h1> +{% endmacro %} + +{% macro html_navigation_links() %} + <nav id="menu" role="navigation"> + <ul> + {% for url, text in navigation_links[lang] %} + {% if url is mapping %} + <li> {{ text }} + <ul> + {% for suburl, text in url %} + {% if rel_link(permalink, suburl) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a></li> + {% else %} + <li><a href="{{ suburl }}">{{ text }}</a></li> + {% endif %} + {% endfor %} + </ul> + {% else %} + {% if rel_link(permalink, url) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a></li> + {% else %} + <li><a href="{{ url }}">{{ text }}</a></li> + {% endif %} + {% endif %} + {% endfor %} + {{ template_hooks['menu']() }} + {{ template_hooks['menu_alt']() }} + </ul> + </nav> +{% endmacro %} + +{% macro html_translation_header() %} + {% if translations|length > 1 %} + <div id="toptranslations"> + <h2>{{ messages("Languages:") }}</h2> + {{ base.html_translations() }} + </div> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/base_helper.tmpl b/nikola/data/themes/base-jinja/templates/base_helper.tmpl new file mode 100644 index 0000000..2dda87b --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/base_helper.tmpl @@ -0,0 +1,103 @@ +{# -*- coding: utf-8 -*- #} + +{% macro html_headstart() %} +<!DOCTYPE html> +<html + +{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook') %} +prefix=' +{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %} +og: http://ogp.me/ns# +{% endif %} +{% if use_open_graph %} +article: http://ogp.me/ns/article# +{% endif %} +{% if comment_system == 'facebook' %} +fb: http://ogp.me/ns/fb# +{% endif %} +' +{% endif %} + +{% if is_rtl %} +dir="rtl" +{% endif %} + +lang="{{ lang }}"> + <head> + <meta charset="utf-8"> + {% if description %} + <meta name="description" content="{{ description }}"> + {% endif %} + <meta name="viewport" content="width=device-width"> + <title>{{ title|e }} | {{ blog_title|e }}</title> + + {{ html_stylesheets() }} + {{ html_feedlinks() }} + {% if permalink %} + <link rel="canonical" href="{{ abs_link(permalink) }}"> + {% endif %} + + {% if favicons %} + {% for name, file, size in favicons %} + <link rel="{{ name }}" href="{{ file }}" sizes="{{ size }}"/> + {% endfor %} + {% endif %} + + {% if comment_system == 'facebook' %} + <meta property="fb:app_id" content="{{ comment_system_id }}"> + {% endif %} + + {{ mathjax_config }} + {% if use_cdn %} + <!--[if lt IE 9]><script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + {% else %} + <!--[if lt IE 9]><script src="/assets/js/html5.js"></script><![endif]--> + {% endif %} + + {{ extra_head_data }} +{% endmacro %} + +{% macro late_load_js() %} + {{ social_buttons_code }} +{% endmacro %} + +{% macro html_stylesheets() %} + {% if use_bundles %} + {% if use_cdn %} + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + {% else %} + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + {% endif %} + {% else %} + <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/theme.css" rel="stylesheet" type="text/css"> + {% if has_custom_css %} + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro html_feedlinks() %} + {% if rss_link %} + {{ rss_link }} + {% elif generate_rss %} + {% if translations|length > 1 %} + {% for language in translations %} + <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}"> + {% endfor %} + {% else %} + <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}"> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro html_translations() %} + <ul class="translations"> + {% for langname in translations.keys() %} + {% if langname != lang %} + <li><a href="{{ _link("index", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> + {% endif %} + {% endfor %} + </ul> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl new file mode 100644 index 0000000..aba7294 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl @@ -0,0 +1,63 @@ +{# -*- coding: utf-8 -*- #} + +{% import 'comments_helper_disqus.tmpl' as disqus with context %} +{% import 'comments_helper_livefyre.tmpl' as livefyre with context %} +{% import 'comments_helper_intensedebate.tmpl' as intensedebate with context %} +{% import 'comments_helper_muut.tmpl' as muut with context %} +{% import 'comments_helper_googleplus.tmpl' as googleplus with context %} +{% import 'comments_helper_facebook.tmpl' as facebook with context %} +{% import 'comments_helper_isso.tmpl' as isso with context %} + +{% macro comment_form(url, title, identifier) %} + {% if comment_system == 'disqus' %} + {{ disqus.comment_form(url, title, identifier) }} + {% elif comment_system == 'livefyre' %} + {{ livefyre.comment_form(url, title, identifier) }} + {% elif comment_system == 'intensedebate' %} + {{ intensedebate.comment_form(url, title, identifier) }} + {% elif comment_system == 'muut' %} + {{ muut.comment_form(url, title, identifier) }} + {% elif comment_system == 'googleplus' %} + {{ googleplus.comment_form(url, title, identifier) }} + {% elif comment_system == 'facebook' %} + {{ facebook.comment_form(url, title, identifier) }} + {% elif comment_system == 'isso' %} + {{ isso.comment_form(url, title, identifier) }} + {% endif %} +{% endmacro %} + +{% macro comment_link(link, identifier) %} + {% if comment_system == 'disqus' %} + {{ disqus.comment_link(link, identifier) }} + {% elif comment_system == 'livefyre' %} + {{ livefyre.comment_link(link, identifier) }} + {% elif comment_system == 'intensedebate' %} + {{ intensedebate.comment_link(link, identifier) }} + {% elif comment_system == 'muut' %} + {{ muut.comment_link(link, identifier) }} + {% elif comment_system == 'googleplus' %} + {{ googleplus.comment_link(link, identifier) }} + {% elif comment_system == 'facebook' %} + {{ facebook.comment_link(link, identifier) }} + {% elif comment_system == 'isso' %} + {{ isso.comment_link(link, identifier) }} + {% endif %} +{% endmacro %} + +{% macro comment_link_script() %} + {% if comment_system == 'disqus' %} + {{ disqus.comment_link_script() }} + {% elif comment_system == 'livefyre' %} + {{ livefyre.comment_link_script() }} + {% elif comment_system == 'intensedebate' %} + {{ intensedebate.comment_link_script() }} + {% elif comment_system == 'muut' %} + {{ muut.comment_link_script() }} + {% elif comment_system == 'googleplus' %} + {{ googleplus.comment_link_script() }} + {% elif comment_system == 'facebook' %} + {{ facebook.comment_link_script() }} + {% elif comment_system == 'isso' %} + {{ isso.comment_link_script() }} + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl new file mode 100644 index 0000000..8288bd4 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl @@ -0,0 +1,44 @@ +{# -*- coding: utf-8 -*- #} +<%! + import json + translations = { + 'es': 'es_ES', + } +%> + +{% macro comment_form(url, title, identifier) %} + {% if comment_system_id %} + <div id="disqus_thread"></div> + <script> + var disqus_shortname ="{{ comment_system_id }}", + {% if url %} + disqus_url="{{ url }}", + {% endif %} + disqus_title={{ title|tojson }}, + disqus_identifier="{{ identifier }}", + disqus_config = function () { + this.language = "{{ translations.get(lang, lang) }}"; + }; + (function() { + var dsq = document.createElement('script'); dsq.async = true; + dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; + (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); + })(); + </script> + <noscript>Please enable JavaScript to view the <a href="//disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript> + <a href="//disqus.com" class="dsq-brlink" rel="nofollow">Comments powered by <span class="logo-disqus">Disqus</span></a> + {% endif %} +{% endmacro %} + +{% macro comment_link(link, identifier) %} + {% if comment_system_id %} + <a href="{{ link }}#disqus_thread" data-disqus-identifier="{{ identifier }}">Comments</a> + {% endif %} +{% endmacro %} + + +{% macro comment_link_script() %} + {% if comment_system_id %} + <script>var disqus_shortname="{{ comment_system_id }}";(function(){var a=document.createElement("script");a.async=true;a.src="//"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(a)}());</script> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_facebook.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_facebook.tmpl new file mode 100644 index 0000000..21dac2a --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_facebook.tmpl @@ -0,0 +1,62 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} +<div id="fb-root"></div> +<script> + window.fbAsyncInit = function() { + // init the FB JS SDK + FB.init({ + appId : '{{ comment_system_id }}', + status : true, + xfbml : true + }); + + }; + + // Load the SDK asynchronously + (function(d, s, id){ + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) {return;} + js = d.createElement(s); js.id = id; + js.src = "//connect.facebook.net/en_US/all.js"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); +</script> + +<div class="fb-comments" data-href="{{ url }}" data-width="470"></div> +{% endmacro %} + +{% macro comment_link(link, identifier) %} +<span class="fb-comments-count" data-url="{{ link }}"> +{% endmacro %} + +{% macro comment_link_script() %} +<div id="fb-root"></div> +<script> + // thank lxml for this + $('.fb-comments-count').each(function(i, obj) { + var url = obj.attributes['data-url'].value; + // change here if you dislike the default way of displaying + // this + obj.innerHTML = '<fb:comments-count href="' + url + '"></fb:comments-count> comments'; + }); + + window.fbAsyncInit = function() { + // init the FB JS SDK + FB.init({ + appId : '{{ comment_system_id }}', + status : true, + xfbml : true + }); + + }; + + // Load the SDK asynchronously + (function(d, s, id){ + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) {return;} + js = d.createElement(s); js.id = id; + js.src = "//connect.facebook.net/en_US/all.js"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); +</script> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl new file mode 100644 index 0000000..cf153e0 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl @@ -0,0 +1,17 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} +<script src="https://apis.google.com/js/plusone.js"></script> +<div class="g-comments" + data-href="{{ url }}" + data-first_party_property="BLOGGER" + data-view_type="FILTERED_POSTMOD"> +</div> +{% endmacro %} + +{% macro comment_link(link, identifier) %} +<div class="g-commentcount" data-href="{{ link }}"></div> +<script src="https://apis.google.com/js/plusone.js"></script> +{% endmacro %} + +{% macro comment_link_script() %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl new file mode 100644 index 0000000..042409b --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl @@ -0,0 +1,25 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} +<script> +var idcomments_acct = '{{ comment_system_id }}'; +var idcomments_post_id = "{{ identifier }}"; +var idcomments_post_url = "{{ url }}"; +</script> +<span id="IDCommentsPostTitle" style="display:none"></span> +<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script> +</script> +{% endmacro %} + +{% macro comment_link(link, identifier) %} +<a href="{link}" onclick="this.href='{{ link }}'; this.target='_self';"><span class='IDCommentsReplace' style='display:none'>{{ identifier }}</span> +<script> +var idcomments_acct = '{{ comment_system_id }}'; +var idcomments_post_id = "{{ identifier }}"; +var idcomments_post_url = "{{ link }}"; +</script> +<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> +</a> +{% endmacro %} + +{% macro comment_link_script() %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl new file mode 100644 index 0000000..22a9595 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl @@ -0,0 +1,20 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} + {% if comment_system_id %} + <div data-title="{{ title|urlencode }}" id="isso-thread"></div> + <script src="{{ comment_system_id }}js/embed.min.js" data-isso="{{ comment_system_id }}"></script> + {% endif %} +{% endmacro %} + +{% macro comment_link(link, identifier) %} + {% if comment_system_id %} + <a href="{{ link }}#isso-thread">Comments</a> + {% endif %} +{% endmacro %} + + +{% macro comment_link_script() %} + {% if comment_system_id %} + <script src="{{ comment_system_id }}js/count.min.js" data-isso="{{ comment_system_id }}"></script> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl new file mode 100644 index 0000000..5b01fbf --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl @@ -0,0 +1,33 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} +<div id="livefyre-comments"></div> +<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script> +<script> +(function () { + var articleId = "{{ identifier }}"; + fyre.conv.load({}, [{ + el: 'livefyre-comments', + network: "livefyre.com", + siteId: "{{ comment_system_id }}", + articleId: articleId, + signed: false, + collectionMeta: { + articleId: articleId, + url: fyre.conv.load.makeCollectionUrl(), + } + }], function() {}); +}()); +</script> +{% endmacro %} + +{% macro comment_link(link, identifier) %} + <a href="{{ link }}"> + <span class="livefyre-commentcount" data-lf-site-id="{{ comment_system_id }}" data-lf-article-id="{{ identifier }}"> + 0 Comments + </span> +{% endmacro %} + + +{% macro comment_link_script() %} +<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl new file mode 100644 index 0000000..8912e19 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl @@ -0,0 +1,5 @@ +{# -*- coding: utf-8 -*- #} +{% import 'comments_helper.tmpl' as comments with context %} +{% if not post.meta('nocomments') %} + {{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }} +{% endif %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_muut.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_muut.tmpl new file mode 100644 index 0000000..79ae523 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_muut.tmpl @@ -0,0 +1,13 @@ +{# -*- coding: utf-8 -*- #} + +{% macro comment_form(url, title, identifier) %} + <a class="muut" href="https://muut.com/i/{{ comment_system_id }}/{{ identifier }}">{{ comment_system_id }} forums</a> +{% endmacro %} + +{% macro comment_link(link, identifier) %} +{% endmacro %} + + +{% macro comment_link_script() %} +<script src="//cdn.muut.com/1/moot.min.js"></script> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/crumbs.tmpl b/nikola/data/themes/base-jinja/templates/crumbs.tmpl new file mode 100644 index 0000000..eede9c2 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/crumbs.tmpl @@ -0,0 +1,13 @@ +{# -*- coding: utf-8 -*- #} + +{% macro bar(crumbs) %} +{% if crumbs %} +<nav class="breadcrumbs"> +<ul class="breadcrumb"> + {% for link, text in crumbs %} + <li><a href="{{ link }}">{{ text }}</a></li> + {% endfor %} +</ul> +</nav> +{% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/gallery.tmpl b/nikola/data/themes/base-jinja/templates/gallery.tmpl new file mode 100644 index 0000000..86eea12 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/gallery.tmpl @@ -0,0 +1,36 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} +{% import 'comments_helper.tmpl' as comments with context %} +{% import 'crumbs.tmpl' as ui with context %} +{% block sourcelink %}{% endblock %} + +{% block content %} + {{ ui.bar(crumbs) }} + {% if title %} + <h1>{{ title }}</h1> + {% endif %} + {% if post %} + <p> + {{ post.text() }} + </p> + {% endif %} + {% if folders %} + <ul> + {% for folder, ftitle in folders %} + <li><a href="{{ folder }}"><i + class="icon-folder-open"></i> {{ ftitle }}</a></li> + {% endfor %} + </ul> + {% endif %} + {% if photo_array %} + <ul class="thumbnails"> + {% for image in photo_array %} + <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title'] }}"> + <img src="{{ image['url_thumb'] }}" alt="{{ image['title'] }}" /></a> + {% endfor %} + </ul> + {% endif %} +{% if site_has_comments and enable_comments %} + {{ comments.comment_form(None, permalink, title) }} +{% endif %} +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/index.tmpl b/nikola/data/themes/base-jinja/templates/index.tmpl new file mode 100644 index 0000000..206fc34 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/index.tmpl @@ -0,0 +1,34 @@ +{# -*- coding: utf-8 -*- #} +{% import 'index_helper.tmpl' as helper with context %} +{% import 'comments_helper.tmpl' as comments with context %} +{% extends 'base.tmpl' %} + +{% block content %} +<div class="postindex"> +{% for post in posts %} + <article class="h-entry post-{{ post.meta('type') }}"> + <header> + <h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title() }}</h1></a> + <div class="metadata"> + <p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p> + <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p> + {% if not post.meta('nocomments') and site_has_comments %} + <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} + {% endif %} + </div> + </header> + {% if index_teasers %} + <div class="p-summary entry-summary"> + {{ post.text(teaser_only=True) }} + {% else %} + <div class="e-content entry-content"> + {{ post.text(teaser_only=False) }} + {% endif %} + </div> + </article> +{% endfor %} +</div> +{{ helper.html_pager() }} +{{ comments.comment_link_script() }} +{{ helper.mathjax_script(posts) }} +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/index_helper.tmpl b/nikola/data/themes/base-jinja/templates/index_helper.tmpl new file mode 100644 index 0000000..2f9e8ea --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/index_helper.tmpl @@ -0,0 +1,27 @@ +{# -*- coding: utf-8 -*- #} +{% macro html_pager() %} + {% if prevlink or nextlink %} + <nav class="postindexpager"> + <ul class="pager"> + {% if prevlink %} + <li class="previous"> + <a href="{{ prevlink }}" rel="prev">{{ messages("Newer posts") }}</a> + </li> + {% endif %} + {% if nextlink %} + <li class="next"> + <a href="{{ nextlink }}" rel="next">{{ messages("Older posts") }}</a> + </li> + {% endif %} + </ul> + </nav> + {% endif %} +{% endmacro %} + +{% macro mathjax_script(posts) %} + {% if posts|selectattr("is_mathjax")|list %} + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});</script> + <script src="/assets/js/mathjax.js"></script> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/list.tmpl b/nikola/data/themes/base-jinja/templates/list.tmpl new file mode 100644 index 0000000..e442864 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/list.tmpl @@ -0,0 +1,19 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} + +{% block content %} +<article class="listpage"> + <header> + <h1>{{ title }}</h1> + </header> + {% if items %} + <ul class="postlist"> + {% for text, link in items %} + <li><a href="{{ link }}">{{ text }}</a> + {% endfor %} + </ul> + {% else %} + <p>{{ messages("Nothing found.") }}</p> + {% endif %} +</article> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/list_post.tmpl b/nikola/data/themes/base-jinja/templates/list_post.tmpl new file mode 100644 index 0000000..b90f237 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/list_post.tmpl @@ -0,0 +1,19 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} + +{% block content %} +<article class="listpage"> + <header> + <h1>{{ title }}</h1> + </header> + {% if posts %} + <ul class="postlist"> + {% for post in posts %} + <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title() }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></li> + {% endfor %} + </ul> + {% else %} + <p>{{ messages("No posts found.") }}</p> + {% endif %} +</article> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/listing.tmpl b/nikola/data/themes/base-jinja/templates/listing.tmpl new file mode 100644 index 0000000..ccbc5ba --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/listing.tmpl @@ -0,0 +1,23 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} +{% import 'crumbs.tmpl' as ui with context %} +{% block content %} +{{ ui.bar(crumbs) }} +{% if folders or files %} +<ul> +{% for name in folders %} + <li><a href="{{ name }}"><i class="icon-folder-open"></i> {{ name }}</a> +{% endfor %} +{% for name in files %} + <li><a href="{{ name }}.html"><i class="icon-file"></i> {{ name }}</a> +{% endfor %} +</ul> +{% endif %} +{% if code %} + {{ code }} +{% endif %} +{% if source_link %} + <p class="sourceline"><a href="{{ source_link }}" id="sourcelink">{{ messages("Source") }}</a></p> +{% endif %} +{% endblock %} + diff --git a/nikola/data/themes/base-jinja/templates/post.tmpl b/nikola/data/themes/base-jinja/templates/post.tmpl new file mode 100644 index 0000000..75c7690 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/post.tmpl @@ -0,0 +1,39 @@ +{# -*- coding: utf-8 -*- #} +{% import 'post_helper.tmpl' as helper with context %} +{% import 'post_header.tmpl' as pheader with context %} +{% import 'comments_helper.tmpl' as comments with context %} +{% extends 'base.tmpl' %} + +{% block extra_head %} + {{ super() }} + {% if post.meta('keywords') %} + <meta name="keywords" content="{{ post.meta('keywords')|e }}"> + {% endif %} + <meta name="author" content="{{ post.author() }}"> + {{ helper.open_graph_metadata(post) }} + {{ helper.twitter_card_information(post) }} + {{ helper.meta_translations(post) }} +{% endblock %} + +{% block content %} +<article class="post-{{ post.meta('type') }} h-entry hentry postpage" itemscope="itemscope" itemtype="http://schema.org/Article"> + {{ pheader.html_post_header() }} + <div class="e-content entry-content" itemprop="articleBody text"> + {{ post.text() }} + </div> + <aside class="postpromonav"> + <nav> + {{ helper.html_tags(post) }} + {{ helper.html_pager(post) }} + </nav> + </aside> + {% if not post.meta('nocomments') and site_has_comments %} + <section class="comments"> + <h2>{{ messages("Comments") }}</h2> + {{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }} + </section> + {% endif %} + {{ helper.mathjax_script(post) }} +</article> +{{ comments.comment_link_script() }} +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/post_header.tmpl b/nikola/data/themes/base-jinja/templates/post_header.tmpl new file mode 100644 index 0000000..0ed40b9 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/post_header.tmpl @@ -0,0 +1,49 @@ +{# -*- coding: utf-8 -*- #} +{% import 'post_helper.tmpl' as helper with context %} +{% import 'comments_helper.tmpl' as comments with context %} + +{% macro html_title() %} +{% if title and not post.meta('hidetitle') %} + <h1 class="p-name entry-title" itemprop="headline name"><a href="{{ post.permalink() }}" class="u-url">{{ title|e }}</a></h1> +{% endif %} +{% endmacro %} + +{% macro html_translations(post) %} + {% if translations|length > 1 %} + <div class="metadata posttranslations translations"> + <h3 class="posttranslations-intro">{{ messages("Also available in:") }}</h3> + {% for langname in translations.keys() %} + {% if langname != lang and post.is_translation_available(langname) %} + <p><a href="{{ post.permalink(langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></p> + {% endif %} + {% endfor %} + </div> + {% endif %} +{% endmacro %} + +{% macro html_sourcelink() %} + {% if show_sourcelink %} + <p class="sourceline"><a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a></p> + {% endif %} +{% endmacro %} + +{% macro html_post_header() %} + <header> + {{ html_title() }} + <div class="metadata"> + <p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p> + <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p> + {% if not post.meta('nocomments') and site_has_comments %} + <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} + {% endif %} + {{ html_sourcelink() }} + {% if post.meta('link') %} + <p><a href='{{ post.meta('link') }}'>{{ messages("Original site") }}</a></p> + {% endif %} + {% if post.description() %} + <meta name="description" itemprop="description" content="{{ post.description() }}"> + {% endif %} + </div> + {{ html_translations(post) }} + </header> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/post_helper.tmpl b/nikola/data/themes/base-jinja/templates/post_helper.tmpl new file mode 100644 index 0000000..c695e57 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/post_helper.tmpl @@ -0,0 +1,76 @@ +{# -*- coding: utf-8 -*- #} + +{% macro meta_translations(post) %} + {% if translations|length > 1 %} + {% for langname in translations.keys() %} + {% if langname != lang and post.is_translation_available(langname) %} + <link rel="alternate" hreflang="{{ langname }}" href="{{ post.permalink(langname) }}"> + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} + +{% macro html_tags(post) %} + {% if post.tags %} + <ul itemprop="keywords" class="tags"> + {% for tag in post.tags %} + <li><a class="tag p-category" href="{{ _link('tag', tag) }}" rel="tag">{{ tag }}</a></li> + {% endfor %} + </ul> + {% endif %} +{% endmacro %} + +{% macro html_pager(post) %} + {% if post.prev_post or post.next_post %} + <ul class="pager"> + {% if post.prev_post %} + <li class="previous"> + <a href="{{ post.prev_post.permalink() }}" rel="prev" title="{{ post.prev_post.title() }}">{{ messages("Previous post") }}</a> + </li> + {% endif %} + {% if post.next_post %} + <li class="next"> + <a href="{{ post.next_post.permalink() }}" rel="next" title="{{ post.next_post.title() }}">{{ messages("Next post") }}</a> + </li> + {% endif %} + </ul> + {% endif %} +{% endmacro %} + +{% macro open_graph_metadata(post) %} + {% if use_open_graph %} + <meta name="og:title" content="{{ post.title()[:70]|e }}"> + <meta name="og:url" content="{{ abs_link(permalink) }}"> + {% if post.description() %} + <meta name="og:description" content="{{ post.description()[:200]|e }}"> + {% else %} + <meta name="og:description" content="{{ post.text(strip_html=True)[:200]|e }}"> + {% endif %} + <meta name="og:site_name" content="{{ blog_title|e }}"> + <meta name="og:type" content="article"> + {% endif %} +{% endmacro %} + +{% macro twitter_card_information(post) %} + {% if twitter_card and twitter_card['use_twitter_cards'] %} + <meta name="twitter:card" content="{{ twitter_card.get('card', 'summary')|e }}"> + {% if 'site:id' in twitter_card %} + <meta name="twitter:site:id" content="{{ twitter_card['site:id'] }}"> + {% elif 'site' in twitter_card %} + <meta name="twitter:site" content="{{ twitter_card['site'] }}"> + {% endif %} + {% if 'creator:id' in twitter_card %} + <meta name="twitter:creator:id" content="{{ twitter_card['creator:id'] }}"> + {% elif 'creator' in twitter_card %} + <meta name="twitter:creator" content="{{ twitter_card['creator'] }}"> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro mathjax_script(post) %} + {% if post.is_mathjax %} + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});</script> + <script src="/assets/js/mathjax.js"></script> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl b/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl new file mode 100644 index 0000000..ceaec3f --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl @@ -0,0 +1,18 @@ +{# -*- coding: utf-8 -*- #} +{% block content %} +<!-- Begin post-list {{ post_list_id }} --> +<div id="{{ post_list_id }}" class="post-list"> + {% if posts %} + <ul class="post-list"> + {% for post in posts %} + <li class="post-list-item"> + {{ post.formatted_date(date_format) }} + + <a href="{{ post.permalink(lang) }}">{{ post.title(lang) }}</a> + </li> + {% endfor %} + </ul> + {% endif %} +</div> +<!-- End post-list {{ post_list_id }} --> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/slides.tmpl b/nikola/data/themes/base-jinja/templates/slides.tmpl new file mode 100644 index 0000000..0ae8fe8 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/slides.tmpl @@ -0,0 +1,24 @@ +{% block content %} +<div id="{{ carousel_id }}" class="carousel slide"> + <ol class="carousel-indicators"> + {% for i in range(slides_content|length) %} + {% if i == 0 %} + <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}" class="active"></li> + {% else %} + <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}"></li> + {% endif %} + {% endfor %} + </ol> + <div class="carousel-inner"> + {% for i, image in enumerate(slides_content) %} + {% if i == 0 %} + <div class="item active"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div> + {% else %} + <div class="item"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div> + {% endif %} + {% endfor %} + </div> + <a class="left carousel-control" href="#{{ carousel_id }}" data-slide="prev">‹</a> + <a class="right carousel-control" href="#{{ carousel_id }}" data-slide="next">›</a> +</div> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/story.tmpl b/nikola/data/themes/base-jinja/templates/story.tmpl new file mode 100644 index 0000000..99caaee --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/story.tmpl @@ -0,0 +1,37 @@ +{# -*- coding: utf-8 -*- #} +{% import 'post_helper.tmpl' as helper with context %} +{% import 'post_header.tmpl' as pheader with context %} +{% import 'comments_helper.tmpl' as comments with context %} +{% extends 'post.tmpl' %} + +{% block extra_head %} + {{ super() }} + {% if post.meta('keywords') %} + <meta name="keywords" content="{{ post.meta('keywords')|e }}"> + {% endif %} + <meta name="author" content="{{ post.author() }}"> + {{ helper.open_graph_metadata(post) }} + {{ helper.twitter_card_information(post) }} + {{ helper.meta_translations(post) }} + {% if post.description() %} + <meta name="description" itemprop="description" content="{{ post.description() }}"> + {% endif %} +{% endblock %} + +{% block content %} +<article class="storypage" itemscope="itemscope" itemtype="http://schema.org/Article"> + <header> + {{ pheader.html_title() }} + {{ pheader.html_translations(post) }} + </header> + <div itemprop="articleBody text"> + {{ post.text() }} + </div> + {% if site_has_comments and enable_comments and not post.meta('nocomments') %} + <section class="comments"> + <h2>{{ messages("Comments") }}</h2> + {{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }} + </section> + {% endif %} +</article> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/tag.tmpl b/nikola/data/themes/base-jinja/templates/tag.tmpl new file mode 100644 index 0000000..84f9e68 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/tag.tmpl @@ -0,0 +1,40 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'list_post.tmpl' %} + +{% block extra_head %} + {{ super() }} + {% if translations|length > 1 and generate_rss %} + {% for language in translations %} + <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for {{ kind }} {{ tag }} ({{ language }})" href="{{ _link(kind + "_rss", tag, language) }}"> + {% endfor %} + {% elif generate_rss %} + <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for {{ kind }} {{ tag }}" href="{{ _link(kind + "_rss", tag) }}"> + {% endif %} +{% endblock %} + + +{% block content %} +<article class="tagpage"> + <header> + <h1>{{ title }}</h1> + <div class="metadata"> + {% if translations|length > 1 and generate_rss %} + {% for language in translations %} + <p class="feedlink"> + <a href="{{ _link(kind + "_rss", tag, language) }}" hreflang="{{ language }}" type="application/rss+xml">{{ messages('RSS feed', language) }} ({{ language }})</a> + </p> + {% endfor %} + {% elif generate_rss %} + <p class="feedlink"><a href="{{ _link(kind + "_rss", tag) }}" type="application/rss+xml">{{ messages('RSS feed') }}</a></p> + {% endif %} + </div> + </header> + {% if posts %} + <ul class="postlist"> + {% for post in posts %} + <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title() }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></li> + {% endfor %} + </ul> + {% endif %} +</article> +{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/tagindex.tmpl b/nikola/data/themes/base-jinja/templates/tagindex.tmpl new file mode 100644 index 0000000..af0a992 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/tagindex.tmpl @@ -0,0 +1,2 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'index.tmpl' %} diff --git a/nikola/data/themes/base-jinja/templates/tags.tmpl b/nikola/data/themes/base-jinja/templates/tags.tmpl new file mode 100644 index 0000000..7bcb7b2 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/tags.tmpl @@ -0,0 +1,30 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} + +{% block content %} +<article class="tagindex"> + <header> + <h1>{{ title }}</h1> + </header> + {% if cat_items %} + <h2>{{ messages("Categories") }}</h2> + <ul class="postlist"> + {% for text, link in cat_items %} + {% if text %} + <li><a class="reference" href="{{ link }}">{{ text }}</a></li> + {% endif %} + {% endfor %} + </ul> + {% if items %} + <h2>{{ messages("Tags") }}</h2> + {% endif %} + {% endif %} + {% if items %} + <ul class="postlist"> + {% for text, link in items %} + <li><a class="reference listtitle" href="{{ link }}">{{ text }}</a></li> + {% endfor %} + </ul> + {% endif %} +</article> +{% endblock %} diff --git a/nikola/data/themes/base/README.md b/nikola/data/themes/base/README.md index f92f490..5d1da94 100644 --- a/nikola/data/themes/base/README.md +++ b/nikola/data/themes/base/README.md @@ -1,4 +1,4 @@ This theme has almost no styling, it's meant as a basis from which other -teams can be developed. +themes can be developed. Therefore, most "advanced" features, such as slides or galleries, are broken. diff --git a/nikola/data/themes/base/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css index 2a924f1..6fd1072 100644 --- a/nikola/data/themes/base/assets/css/theme.css +++ b/nikola/data/themes/base/assets/css/theme.css @@ -1 +1,255 @@ -/* This file intentionally left blank. */ +@charset "UTF-8"; + +/* + Copyright © 2014 Daniel Aleksandersen 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. +*/ + +body { + color: #4F5151; + font-family: Helvetica, Arial, sans-serif; + font-size: 17px; + line-height: 1.4; + padding: 1em; +} +@media print { + body { + font-family: Garamond, serif; + } +} + +#container { + margin: 1em auto; + max-width: 770px; +} +#menu ul, +#menu ul li, +.postpromonav .tags, +.postpromonav .tags li, +.pager, +.pager li, +#toptranslations ul, +#toptranslations ul li { + list-style: none; + padding-left: 0; + padding-right: 0; +} + +#toptranslations ul { + display: inline; +} + +#menu ul li, +#toptranslations ul li { + display: inline-block; + margin-right: 1.5em; +} + +#toptranslations h2 { + display: inline; + font-size: 1em; + margin-right: 1.5em; +} + +#menu ul li:dir(rtl), +#toptranslations ul li:dir(rtl), +#toptranslations h2:dir(rtl) { + margin-left: 1.5em; + margin-right: 0; +} + +#toptranslations { + text-align: right; + float: right; +} + +#toptranslations:dir(rtl) { + text-align: left; + float: left; +} + +.posttranslations h3 { + display: inline; + font-size: 1em; +} + +.entry-title { + font-size: 2em; +} + +.posttranslations h3:last-child { + display: none; +} + +.postindex article { + border-bottom: 1px solid #4F5151; + padding-bottom: 1em; +} +#header { + border-bottom: 1px solid #4F5151; +} +#footer { + border-top: 1px solid #4F5151; +} + +/* Tags */ +.postpromonav { + border-bottom: 1px solid #4F5151; + border-top: 1px solid #4F5151; + margin-top: 1em; + padding: .5em 0; +} +.postpromonav .tags { + text-align: center; +} +.metadata p:before, +.postpromonav .tags li:before, +.postlist .listdate:before { + content: " — "; +} +.postlist li { + margin-bottom: .33em; +} + +/* Post and archive pagers */ +.postindexpager .pager .next:before { + content: "↓ "; +} +.postindexpager .pager .previous:before { + content: "↑ "; +} +.postpromonav .pager .next:after { + content: " →"; +} +.postpromonav .pager .previous:dir(rtl):after { + content: " →"; +} +.postpromonav .pager .previous:before { + content: "← "; +} +.postpromonav .pager .next:dir(rtl):before { + content: "← "; +} + +.metadata p:first-of-type:before, +.postpromonav .tags li:first-of-type:before { + content: ""; +} +.postpromonav .pager { + height: 1em; +} +.postpromonav .tags li, +.postpromonav .pager li { + display: inline-block; +} +.postpromonav .pager .next { + float: right; +} +.postpromonav .pager .next:dir(rtl) { + float: left; +} +.metadata p { + display: inline; +} + +#brand { + font-size: 3em; + line-height: 1; +} + +/* Links */ +:link { + color: #1168CC; + text-decoration: none; +} +:visited { + color: #6830BB; +} +:link:hover, :visited:hover { + color: #0d53a3; +} + +#brand :link, +#brand :visited { + color: inherit; +} + +/* Images */ +img { + border: none; + line-height: 1; +} + +.postpage img, +.postpage object, +.postindex article img, +.postindex article object { + height: auto; + max-width: 100%; +} + +/* Comment helpers */ +#disqus_thread { + min-height: 325px; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; +} + +.breadcrumb > li { + display: inline-block; + margin-right: 0; + margin-left: 0; +} + +.breadcrumb > li:after { + content: ' / '; + color: #888; +} + +.breadcrumb > li:last-of-type:after { + content: ''; + margin-left: 0; +} + +.thumbnails { + list-style: none; + padding: 0; +} + +.thumbnails > li { + display: inline-block; + margin-right: 10px; +} + +.thumbnails > li:last-of-type { + margin-right: 0; +} + +.codetable .linenos { + padding-right: 10px; +} diff --git a/nikola/data/themes/base/assets/js/html5.js b/nikola/data/themes/base/assets/js/html5.js new file mode 100644 index 0000000..448cebd --- /dev/null +++ b/nikola/data/themes/base/assets/js/html5.js @@ -0,0 +1,8 @@ +/* + HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); +a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>"; +c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| +"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); +if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document); diff --git a/nikola/data/themes/base/assets/js/mathjax.js b/nikola/data/themes/base/assets/js/mathjax.js index 82c1f6c..5e14369 100644 --- a/nikola/data/themes/base/assets/js/mathjax.js +++ b/nikola/data/themes/base/assets/js/mathjax.js @@ -5,7 +5,6 @@ window.onload = function () { setTimeout(function () { var script = document.createElement("script"); - script.type = "text/javascript"; if (location.protocol == 'https:') { scriptbase = "https://c328740.ssl.cf1.rackcdn.com/"; } else { diff --git a/nikola/data/themes/base/bundles b/nikola/data/themes/base/bundles index 4760181..d87b458 100644 --- a/nikola/data/themes/base/bundles +++ b/nikola/data/themes/base/bundles @@ -1,2 +1,2 @@ -assets/css/all.css=rst.css,code.css,theme.css -assets/css/all-nocdn.css=rst.css,code.css,theme.css +assets/css/all.css=rst.css,code.css,theme.css,custom.css +assets/css/all-nocdn.css=rst.css,code.css,theme.css,custom.css diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py index 6e85212..4158ac8 100644 --- a/nikola/data/themes/base/messages/messages_bg.py +++ b/nikola/data/themes/base/messages/messages_bg.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Също достъпно в", + "%d min remaining to read": "", "Also available in:": "Също достъпно в:", "Archive": "Архив", "Categories": "Категории", + "Comments": "", "LANGUAGE": "Български", + "Languages:": "", "More posts about %s": "Още публикации относно %s", - "More posts about": "Още публикации относно", "Newer posts": "Нови публикации", "Next post": "Следваща публикация", "No posts found.": "", "Nothing found.": "", "Older posts": "Стари публикации", "Original site": "Оригиналния сайт", - "Posted": "Публиковано", "Posted:": "Публиковано:", "Posts about %s": "Публикации относно %s", "Posts for year %s": "Публикации за %s година", "Posts for {month} {year}": "Публикации за {month} {year}", "Previous post": "Предишна публикация", + "Publication date": "", + "RSS feed": "", "Read in English": "Прочетете на български", "Read more": "Прочети още", "Source": "Source", diff --git a/nikola/data/themes/base/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py index 220d571..7723f3e 100644 --- a/nikola/data/themes/base/messages/messages_ca.py +++ b/nikola/data/themes/base/messages/messages_ca.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "També disponibles en", + "%d min remaining to read": "", "Also available in:": "També disponibles en:", "Archive": "Arxiu", "Categories": "", + "Comments": "", "LANGUAGE": "Català", + "Languages:": "", "More posts about %s": "Més entrades sobre %s", - "More posts about": "Més entrades sobre", "Newer posts": "Entrades posteriors", "Next post": "Entrada següent", "No posts found.": "", "Nothing found.": "", "Older posts": "Entrades anteriors", "Original site": "Lloc original", - "Posted": "Publicat", "Posted:": "Publicat:", "Posts about %s": "Entrades sobre %s", "Posts for year %s": "Entrades de l'any %s", "Posts for {month} {year}": "", "Previous post": "Entrada anterior", + "Publication date": "", + "RSS feed": "", "Read in English": "Llegeix-ho en català", "Read more": "Llegeix-ne més", "Source": "Codi", diff --git a/nikola/data/themes/base/messages/messages_cs.py b/nikola/data/themes/base/messages/messages_cs.py index f66c2c4..f80a79f 100644 --- a/nikola/data/themes/base/messages/messages_cs.py +++ b/nikola/data/themes/base/messages/messages_cs.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Dostupné také v", + "%d min remaining to read": "", "Also available in:": "Dostupné také v", "Archive": "Archiv", "Categories": "Kategorie", + "Comments": "", "LANGUAGE": "Čeština", + "Languages:": "", "More posts about %s": "Další příspěvky o %s", - "More posts about": "Další příspěvky o", "Newer posts": "Novější příspěvky", "Next post": "Další příspěvek", "No posts found.": "", "Nothing found.": "", "Older posts": "Starší příspěvky", "Original site": "Původní stránka", - "Posted": "Zveřejněno", "Posted:": "Zveřejněno:", "Posts about %s": "Příspěvky o %s", "Posts for year %s": "Příspěvky v roce %s", "Posts for {month} {year}": "Příspěvky v {month} {year}", "Previous post": "Předchozí příspěvek", + "Publication date": "", + "RSS feed": "", "Read in English": "Číst v češtině", "Read more": "Číst dál", "Source": "Zdroj", diff --git a/nikola/data/themes/base/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py index 41fe015..737e63b 100644 --- a/nikola/data/themes/base/messages/messages_de.py +++ b/nikola/data/themes/base/messages/messages_de.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Auch verfügbar in", + "%d min remaining to read": "", "Also available in:": "Auch verfügbar in:", "Archive": "Archiv", "Categories": "Kategorien", + "Comments": "Kommentare", "LANGUAGE": "Deutsch", + "Languages:": "Sprachen:", "More posts about %s": "Weitere Einträge über %s", - "More posts about": "Weitere Einträge über", "Newer posts": "Neuere Einträge", "Next post": "Nächster Eintrag", - "No posts found.": "Keine einträge gefunden.", + "No posts found.": "Keine Einträge gefunden.", "Nothing found.": "Nichts gefunden.", "Older posts": "Ältere Einträge", "Original site": "Original-Seite", - "Posted": "Veröffentlicht", "Posted:": "Veröffentlicht:", "Posts about %s": "Einträge über %s", "Posts for year %s": "Einträge aus dem Jahr %s", "Posts for {month} {year}": "Einträge aus {month} {year}", "Previous post": "Vorheriger Eintrag", + "Publication date": "Veröffentlichungsdatum", + "RSS feed": "RSS-Feed", "Read in English": "Auf Deutsch lesen", "Read more": "Weiterlesen", "Source": "Source", diff --git a/nikola/data/themes/base/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py index f658fa0..aeca302 100644 --- a/nikola/data/themes/base/messages/messages_el.py +++ b/nikola/data/themes/base/messages/messages_el.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Διαθέσιμο και στα", + "%d min remaining to read": "", "Also available in:": "Διαθέσιμο και στα:", "Archive": "Αρχείο", "Categories": "Κατηγορίες", + "Comments": "", "LANGUAGE": "Ελληνικά", + "Languages:": "", "More posts about %s": "Περισσότερες αναρτήσεις για %s", - "More posts about": "Περισσότερες αναρτήσεις για", "Newer posts": "Νεότερες αναρτήσεις", "Next post": "Επόμενη ανάρτηση", "No posts found.": "", "Nothing found.": "", "Older posts": "Παλαιότερες αναρτήσεις", "Original site": "Ιστοσελίδα αρχικής ανάρτησης", - "Posted": "Αναρτήθηκε", "Posted:": "Αναρτήθηκε:", "Posts about %s": "Αναρτήσεις για %s", "Posts for year %s": "Αναρτήσεις για το έτος %s", "Posts for {month} {year}": "Αναρτήσεις για τον {month} του {year}", "Previous post": "Προηγούμενη ανάρτηση", + "Publication date": "", + "RSS feed": "", "Read in English": "Διαβάστε στα Ελληνικά", "Read more": "Διαβάστε περισσότερα", "Source": "Πηγαίος κώδικας", diff --git a/nikola/data/themes/base/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py index e2bff53..bdf2d42 100644 --- a/nikola/data/themes/base/messages/messages_en.py +++ b/nikola/data/themes/base/messages/messages_en.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Also available in", + "%d min remaining to read": "%d min remaining to read", "Also available in:": "Also available in:", "Archive": "Archive", "Categories": "Categories", + "Comments": "Comments", "LANGUAGE": "English", + "Languages:": "Languages:", "More posts about %s": "More posts about %s", - "More posts about": "More posts about", "Newer posts": "Newer posts", "Next post": "Next post", "No posts found.": "No posts found.", "Nothing found.": "Nothing found.", "Older posts": "Older posts", "Original site": "Original site", - "Posted": "Posted", "Posted:": "Posted:", "Posts about %s": "Posts about %s", "Posts for year %s": "Posts for year %s", "Posts for {month} {year}": "Posts for {month} {year}", "Previous post": "Previous post", + "Publication date": "Publication date", + "RSS feed": "RSS feed", "Read in English": "Read in English", "Read more": "Read more", "Source": "Source", diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py index f59a441..e439e6b 100644 --- a/nikola/data/themes/base/messages/messages_eo.py +++ b/nikola/data/themes/base/messages/messages_eo.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Ankaŭ disponebla en", + "%d min remaining to read": "", "Also available in:": "Ankaŭ disponebla en:", "Archive": "Arĥivo", "Categories": "Kategorioj", + "Comments": "", "LANGUAGE": "Anglalingve", + "Languages:": "", "More posts about %s": "Pli artikoloj pri %s", - "More posts about": "Pli artikoloj pri", "Newer posts": "Pli novaj artikoloj", "Next post": "Venonta artikolo", "No posts found.": "", "Nothing found.": "", "Older posts": "Pli malnovaj artikoloj", "Original site": "Originala interretejo", - "Posted": "Skribita", "Posted:": "Skribita:", "Posts about %s": "Artikoloj pri %s", "Posts for year %s": "Artikoloj de la jaro %s", "Posts for {month} {year}": "Artikoloj skribitaj en {month} {year}", "Previous post": "Antaŭa artikolo", + "Publication date": "", + "RSS feed": "", "Read in English": "Legu ĝin en Esperanto", "Read more": "Legu plu", "Source": "Fonto", diff --git a/nikola/data/themes/base/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py index 1923683..0905f00 100644 --- a/nikola/data/themes/base/messages/messages_es.py +++ b/nikola/data/themes/base/messages/messages_es.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "También disponible en", + "%d min remaining to read": "restan %d minutos", "Also available in:": "También disponible en:", "Archive": "Archivo", "Categories": "Categorías", + "Comments": "Comentarios", "LANGUAGE": "Español", + "Languages:": "Idiomas:", "More posts about %s": "Más posts sobre %s", - "More posts about": "Más posts sobre", "Newer posts": "Posts posteriores", "Next post": "Siguiente post", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "No se encontraron posts", + "Nothing found.": "No encontrado", "Older posts": "Posts anteriores", "Original site": "Sitio original", - "Posted": "Publicado", "Posted:": "Publicado:", "Posts about %s": "Posts sobre %s", "Posts for year %s": "Posts del año %s", "Posts for {month} {year}": "Posts de {month} {year}", "Previous post": "Post anterior", + "Publication date": "Fecha de publicación", + "RSS feed": "feed RSS", "Read in English": "Leer en español", "Read more": "Leer más", "Source": "Código", diff --git a/nikola/data/themes/base/messages/messages_et.py b/nikola/data/themes/base/messages/messages_et.py index 058ab5f..f473985 100644 --- a/nikola/data/themes/base/messages/messages_et.py +++ b/nikola/data/themes/base/messages/messages_et.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Saadaval ka", + "%d min remaining to read": "", "Also available in:": "Saadaval ka:", "Archive": "Arhiiv", "Categories": "Kategooriad", + "Comments": "", "LANGUAGE": "Eesti", + "Languages:": "", "More posts about %s": "Veel postitusi %s kohta", - "More posts about": "Veel postitusi kohta", "Newer posts": "Uued postitused", "Next post": "Järgmine postitus", "No posts found.": "", "Nothing found.": "", "Older posts": "Vanemad postitused", "Original site": "Algallikas", - "Posted": "Postitatud", "Posted:": "Postitatud:", "Posts about %s": "Postitused %s kohta", "Posts for year %s": "Postitused aastast %s", "Posts for {month} {year}": "Postitused {year} aasta kuust {month} ", "Previous post": "Eelmine postitus", + "Publication date": "", + "RSS feed": "", "Read in English": "Loe eesti keeles", "Read more": "Loe veel", "Source": "Lähtekood", diff --git a/nikola/data/themes/base/messages/messages_eu.py b/nikola/data/themes/base/messages/messages_eu.py index a8eb743..8958d42 100644 --- a/nikola/data/themes/base/messages/messages_eu.py +++ b/nikola/data/themes/base/messages/messages_eu.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Eskuragarria hemen ere", + "%d min remaining to read": "", "Also available in:": "Eskuragarria hemen ere:", "Archive": "Artxiboa", "Categories": "Kategoriak", + "Comments": "", "LANGUAGE": "Euskara", + "Languages:": "", "More posts about %s": "%s-ri buruzko post gehiago", - "More posts about": "-ri buruzko post gehiago", "Newer posts": "Post berrienak", "Next post": "Hurrengo posta", "No posts found.": "", "Nothing found.": "", "Older posts": "Post zaharrenak", "Original site": "Jatorrizko orria", - "Posted": "Argitaratuta", "Posted:": "Argitaratuta:", "Posts about %s": "%s-ri buruzko postak", "Posts for year %s": "%s. urteko postak", "Posts for {month} {year}": "{year}ko {month}ren postak", "Previous post": "Aurreko posta", + "Publication date": "", + "RSS feed": "", "Read in English": "Euskaraz irakurri", "Read more": "Irakurri gehiago", "Source": "Iturria", diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py index 4475e1b..49cfda5 100644 --- a/nikola/data/themes/base/messages/messages_fa.py +++ b/nikola/data/themes/base/messages/messages_fa.py @@ -2,30 +2,32 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "همچنین قابل دسترس از", + "%d min remaining to read": "", "Also available in:": "همچنین قابل دسترس از:", "Archive": "آرشیو", "Categories": "دستهها", + "Comments": "دیدگاهها", "LANGUAGE": "فارسی", + "Languages:": "زبانها:", "More posts about %s": "ارسالهای بیشتر دربارهٔ%s", - "More posts about": "ارسالهای بیشتر دربارهٔ", "Newer posts": "ارسالهای جدیدتر", "Next post": "ارسال بعدی", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "هیچ پستی پیدا نشد.", + "Nothing found.": "هیچچیزی پیدا نشد.", "Older posts": "پستهای قدیمیتر", "Original site": "سایت اصلی", - "Posted": "ارسال شده", "Posted:": "ارسال شده:", "Posts about %s": "ارسالها دربارهٔ %s", "Posts for year %s": "ارسالها برای سال %s", "Posts for {month} {year}": "ارسال برای {month} {year}", "Previous post": "ارسال پیشین", + "Publication date": "تاریخ انتشار", + "RSS feed": "خوراک", "Read in English": "به فارسی بخوانید", "Read more": "بیشتر بخوانید", "Source": "منبع", "Tags and Categories": "برچسبها و دستهها", "Tags": "برچسبها", "old posts, page %d": "صفحهٔ ارسالهای قدیمی %d", - "page %d": "", + "page %d": "برگه %d", } diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py index 42e6fa2..b621459 100644 --- a/nikola/data/themes/base/messages/messages_fi.py +++ b/nikola/data/themes/base/messages/messages_fi.py @@ -2,30 +2,32 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Saatavilla myös", + "%d min remaining to read": "", "Also available in:": "Saatavilla myös:", "Archive": "Arkisto", "Categories": "Kategoriat", + "Comments": "Kommentit", "LANGUAGE": "Suomi", + "Languages:": "Kielet:", "More posts about %s": "Lisää postauksia aiheesta %s", - "More posts about": "Lisää postauksia aiheesta", "Newer posts": "Uudempia postauksia", "Next post": "Seuraava postaus", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Postauksia ei löytynyt.", + "Nothing found.": "Ei hakutuloksia.", "Older posts": "Vanhempia postauksia", "Original site": "Alkuperäinen sivusto", - "Posted": "Postattu", "Posted:": "Postattu:", "Posts about %s": "Postauksia aiheesta %s", "Posts for year %s": "Postauksia vuodelta %s", "Posts for {month} {year}": "Postauksia ajalle {month} {year}", "Previous post": "Vanhempia postauksia", + "Publication date": "Julkaisupäivämäärä", + "RSS feed": "RSS syöte", "Read in English": "Lue suomeksi", "Read more": "Lue lisää", "Source": "Lähde", "Tags and Categories": "Tagit ja kategoriat", "Tags": "Tagit", - "old posts, page %d": "vanhojen postauksien, sivu %d", + "old posts, page %d": "vanhoja postauksia, sivu %d", "page %d": "sivu %d", } diff --git a/nikola/data/themes/base/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py index 484d695..316ba20 100644 --- a/nikola/data/themes/base/messages/messages_fr.py +++ b/nikola/data/themes/base/messages/messages_fr.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Egalement disponible en", + "%d min remaining to read": "", "Also available in:": "Egalement disponible en:", "Archive": "Archives", "Categories": "Catégories", + "Comments": "Commentaires", "LANGUAGE": "Français", + "Languages:": "Langues:", "More posts about %s": "Plus d'articles sur %s", - "More posts about": "Plus d'articles sur", "Newer posts": "Billets récents", "Next post": "Article suivant", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Pas de billets.", + "Nothing found.": "Pas de résultats.", "Older posts": "Anciens articles", "Original site": "Site d'origine", - "Posted": "Publié", "Posted:": "Publié:", "Posts about %s": "Articles sur %s", "Posts for year %s": "Articles de l'année %s", "Posts for {month} {year}": "Articles de {month} {year}", "Previous post": "Article précédent", + "Publication date": "Date de publication", + "RSS feed": "Flux RSS", "Read in English": "Lire en français", "Read more": "Lire la suite", "Source": "Source", diff --git a/nikola/data/themes/base/messages/messages_hi.py b/nikola/data/themes/base/messages/messages_hi.py index f72d5af..6b53e01 100644 --- a/nikola/data/themes/base/messages/messages_hi.py +++ b/nikola/data/themes/base/messages/messages_hi.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "उपलब्ध भाषाएँ", + "%d min remaining to read": "", "Also available in:": "उपलब्ध भाषाएँ:", "Archive": "आर्काइव", "Categories": "श्रेणियाँ", + "Comments": "", "LANGUAGE": "हिन्दी", + "Languages:": "", "More posts about %s": "%s के बारे में अौर पोस्टें", - "More posts about": " के बारे में अौर पोस्टें", "Newer posts": "नई पोस्टें", "Next post": "अगली पोस्ट", "No posts found.": "", "Nothing found.": "", "Older posts": "पुरानी पोस्टें", "Original site": "असली साइट", - "Posted": "पोस्टेड", "Posted:": "पोस्टेड:", "Posts about %s": "%s के बारे में पोस्टें", "Posts for year %s": "साल %s की पोस्टें", "Posts for {month} {year}": "{month} {year} की पोस्टें", "Previous post": "पिछली पोस्ट", + "Publication date": "", + "RSS feed": "", "Read in English": "हिन्दी में पढ़िए", "Read more": "और पढ़िए", "Source": "सोर्स", diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py index ee5ce41..c3343c9 100644 --- a/nikola/data/themes/base/messages/messages_hr.py +++ b/nikola/data/themes/base/messages/messages_hr.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Također dostupno i u", + "%d min remaining to read": "", "Also available in:": "Također dostupno i u:", "Archive": "Arhiva", "Categories": "Kategorije", + "Comments": "Komentari", "LANGUAGE": "hrvatski", + "Languages:": "Jezici:", "More posts about %s": "Više postova o %s", - "More posts about": "Više postova o", "Newer posts": "Noviji postovi", "Next post": "Sljedeći post", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Nema postova.", + "Nothing found.": "Nema ničeg.", "Older posts": "Stariji postovi", "Original site": "Izvorna stranica", - "Posted": "Objavljeno", "Posted:": "Objavljeno:", "Posts about %s": "Postovi o %s", "Posts for year %s": "Postovi za godinu %s", "Posts for {month} {year}": "Postovi za {month} {year}", "Previous post": "Prethodni post", + "Publication date": "Nadnevak objave", + "RSS feed": "RSS kanal", "Read in English": "Čitaj na hrvatskom", "Read more": "Čitaj dalje", "Source": "Izvor", diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py index 87e25e5..b248d34 100644 --- a/nikola/data/themes/base/messages/messages_it.py +++ b/nikola/data/themes/base/messages/messages_it.py @@ -2,27 +2,29 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Anche disponibile in", + "%d min remaining to read": "ancora %d minuti", "Also available in:": "Anche disponibile in:", "Archive": "Archivio", "Categories": "Categorie", + "Comments": "Commenti", "LANGUAGE": "Italiano", + "Languages:": "Lingue:", "More posts about %s": "Altri articoli collegati %s", - "More posts about": "Altri articoli collegati", "Newer posts": "Articoli recenti", "Next post": "Articolo successivo", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Nessun articolo trovato.", + "Nothing found.": "Non trovato.", "Older posts": "Articoli precedenti", "Original site": "Sito originale", - "Posted": "Pubblicato", "Posted:": "Pubblicato:", "Posts about %s": "Articoli su %s", "Posts for year %s": "Articoli per l'anno %s", "Posts for {month} {year}": "Articoli per {month} {year}", "Previous post": "Articolo precedente", + "Publication date": "Data di pubblicazione", + "RSS feed": "Flusso RSS", "Read in English": "Leggi in italiano", - "Read more": "Espandi", + "Read more": "Continua la lettura", "Source": "Sorgente", "Tags and Categories": "Tags e Categorie", "Tags": "Tags", diff --git a/nikola/data/themes/base/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py index 2df16a4..4b0fd54 100644 --- a/nikola/data/themes/base/messages/messages_ja.py +++ b/nikola/data/themes/base/messages/messages_ja.py @@ -2,30 +2,32 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "他の言語で読む", + "%d min remaining to read": "", "Also available in:": "他の言語で読む:", "Archive": "過去の記事", "Categories": "カテゴリー", + "Comments": "コメント", "LANGUAGE": "日本語", + "Languages:": "言語 :", "More posts about %s": "タグ: %s", - "More posts about": "タグ:", "Newer posts": "新しい記事", "Next post": "次の記事", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "記事はありません", + "Nothing found.": "なにも見つかりませんでした", "Older posts": "過去の記事", "Original site": "元のサイト", - "Posted": "投稿日時", "Posted:": "投稿日時:", "Posts about %s": "%sについての記事", "Posts for year %s": "%s年の記事", "Posts for {month} {year}": "{year}年{month}月の記事", "Previous post": "前の記事", + "Publication date": "投稿日", + "RSS feed": "RSS フィード", "Read in English": "日本語で読む", "Read more": "続きを読む", "Source": "ソース", "Tags and Categories": "タグとカテゴリー", "Tags": "タグ", "old posts, page %d": "前の記事 %dページ目", - "page %d": "", + "page %d": "ページ %d", } diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py index 44fde8a..f6232df 100644 --- a/nikola/data/themes/base/messages/messages_nb.py +++ b/nikola/data/themes/base/messages/messages_nb.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Også tilgjengelig på", + "%d min remaining to read": "", "Also available in:": "Også tilgjengelig på:", "Archive": "Arkiv", "Categories": "Kategorier", + "Comments": "", "LANGUAGE": "norsk", + "Languages:": "", "More posts about %s": "Flere innlegg om %s", - "More posts about": "Flere innlegg om", "Newer posts": "Nyere innlegg", "Next post": "Neste innlegg", "No posts found.": "", "Nothing found.": "", "Older posts": "Eldre innlegg", "Original site": "Opprinnelig side", - "Posted": "Publisert", "Posted:": "Publisert:", "Posts about %s": "Innlegg om %s", "Posts for year %s": "Innlegg fra %s", "Posts for {month} {year}": "Innlegg fra {month} {year}", "Previous post": "Forrige innlegg", + "Publication date": "", + "RSS feed": "", "Read in English": "Les på norsk", "Read more": "Les mer", "Source": "Kilde", diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py index 1952d2e..7cba96b 100644 --- a/nikola/data/themes/base/messages/messages_nl.py +++ b/nikola/data/themes/base/messages/messages_nl.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Ook beschikbaar in", + "%d min remaining to read": "%d min resterende leestijd ", "Also available in:": "Ook beschikbaar in:", "Archive": "Archief", "Categories": "Categorieën", + "Comments": "Commentaar", "LANGUAGE": "Nederlands", + "Languages:": "Talen:", "More posts about %s": "Meer berichten over %s", - "More posts about": "Meer berichten over", "Newer posts": "Nieuwere berichten", "Next post": "Volgend bericht", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Geen berichten gevonden.", + "Nothing found.": "Niets gevonden.", "Older posts": "Oudere berichten", "Original site": "Originele site", - "Posted": "Geplaatst", "Posted:": "Geplaatst:", "Posts about %s": "Berichten over %s", "Posts for year %s": "Berichten voor het jaar %s", "Posts for {month} {year}": "Berichten voor {month} {year}", "Previous post": "Vorig bericht", + "Publication date": "Publicatiedatum", + "RSS feed": "RSS-feed", "Read in English": "Lees in het Nederlands", "Read more": "Lees verder", "Source": "Bron", diff --git a/nikola/data/themes/base/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py index a1183ba..6b6e48d 100644 --- a/nikola/data/themes/base/messages/messages_pl.py +++ b/nikola/data/themes/base/messages/messages_pl.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Również dostępny w językach", + "%d min remaining to read": "zostało %d minut czytania", "Also available in:": "Również dostępny w językach:", "Archive": "Archiwum", "Categories": "Kategorie", + "Comments": "Komentarze", "LANGUAGE": "polski", + "Languages:": "Języki:", "More posts about %s": "Więcej postów o %s", - "More posts about": "Więcej postów o", "Newer posts": "Nowsze posty", "Next post": "Następny post", "No posts found.": "Nie znaleziono żadnych postów.", "Nothing found.": "Nic nie znaleziono.", "Older posts": "Starsze posty", "Original site": "Oryginalna strona", - "Posted": "Opublikowano", "Posted:": "Opublikowano:", "Posts about %s": "Posty o %s", "Posts for year %s": "Posty z roku %s", "Posts for {month} {year}": "Posty z {month} {year}", "Previous post": "Poprzedni post", + "Publication date": "Data publikacji", + "RSS feed": "Kanał RSS", "Read in English": "Czytaj po polsku", "Read more": "Czytaj więcej", "Source": "Źródło", diff --git a/nikola/data/themes/base/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py index bf515e4..c86b2f8 100644 --- a/nikola/data/themes/base/messages/messages_pt_br.py +++ b/nikola/data/themes/base/messages/messages_pt_br.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Também disponível em", + "%d min remaining to read": "%d mín restante para leitura", "Also available in:": "Também disponível em:", "Archive": "Arquivo", "Categories": "Categorias", + "Comments": "Comentários", "LANGUAGE": "Português", + "Languages:": "Idiomas:", "More posts about %s": "Mais posts sobre %s", - "More posts about": "Mais posts sobre", "Newer posts": "Posts mais recentes", "Next post": "Próximo post", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Nenhum tópico encontrado.", + "Nothing found.": "Nada encontrado.", "Older posts": "Posts mais antigos", "Original site": "Site original", - "Posted": "Publicado", "Posted:": "Publicado:", "Posts about %s": "Posts sobre %s", "Posts for year %s": "Posts do ano %s", "Posts for {month} {year}": "Posts de {month} {year}", "Previous post": "Post anterior", + "Publication date": "Data de publicação", + "RSS feed": "Feed RSS", "Read in English": "Ler em português", "Read more": "Leia mais", "Source": "Código", diff --git a/nikola/data/themes/base/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py index fb33b85..7c038cc 100644 --- a/nikola/data/themes/base/messages/messages_ru.py +++ b/nikola/data/themes/base/messages/messages_ru.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Также доступно на", + "%d min remaining to read": "%d минут чтения осталось", "Also available in:": "Также доступно на:", "Archive": "Архив", "Categories": "Категории", + "Comments": "Комментарии", "LANGUAGE": "Русский", + "Languages:": "Языки:", "More posts about %s": "Больше записей о %s", - "More posts about": "Больше записей о", "Newer posts": "Новые записи", "Next post": "Следующая запись", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Записей не найдено.", + "Nothing found.": "Ничего не найдено.", "Older posts": "Старые записи", "Original site": "Оригинальный сайт", - "Posted": "Опубликовано", "Posted:": "Опубликовано:", "Posts about %s": "Записи о %s", "Posts for year %s": "Записи за %s год", "Posts for {month} {year}": "Записи за {month} {year}", "Previous post": "Предыдущая запись", + "Publication date": "Дата опубликования", + "RSS feed": "RSS лента", "Read in English": "Прочесть по-русски", "Read more": "Читать далее", "Source": "Источник", diff --git a/nikola/data/themes/base/messages/messages_sk.py b/nikola/data/themes/base/messages/messages_sk.py new file mode 100644 index 0000000..3b56a58 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_sk.py @@ -0,0 +1,33 @@ +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "%d min remaining to read": "", + "Also available in:": "Tiež dostupné v:", + "Archive": "Archív", + "Categories": "Kategórie", + "Comments": "Komentáre", + "LANGUAGE": "Slovenčina", + "Languages:": "Jazyky:", + "More posts about %s": "Viac príspevkov o %s", + "Newer posts": "Novšie príspevky", + "Next post": "Nasledujúci príspevok", + "No posts found.": "Žiadne príspevky nenájdené", + "Nothing found.": "Nič nenájdené.", + "Older posts": "Staršie príspevky", + "Original site": "Pôvodná stránka", + "Posted:": "Zverejnené:", + "Posts about %s": "Príspevky o %s", + "Posts for year %s": "Príspevky z roku %s", + "Posts for {month} {year}": "Príspevky za mesiac {month} z roku {year}", + "Previous post": "Predchádzajúci príspevok", + "Publication date": "Dátum zverejnenia", + "RSS feed": "RSS kanál", + "Read in English": "Čítať v slovenčine", + "Read more": "Čítať ďalej", + "Source": "Zdroj", + "Tags and Categories": "Štítky a kategórie", + "Tags": "Štítky", + "old posts, page %d": "staré príspevky, strana %d", + "page %d": "stránka %d", +} diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py index 92ad483..53045e3 100644 --- a/nikola/data/themes/base/messages/messages_sl.py +++ b/nikola/data/themes/base/messages/messages_sl.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "Na voljo tudi v", + "%d min remaining to read": "za prebrati preostalo še %d min", "Also available in:": "Na voljo tudi v:", "Archive": "Arhiv", "Categories": "Kategorije", + "Comments": "Komentarji", "LANGUAGE": "Slovenščina", + "Languages:": "Jeziki:", "More posts about %s": "Več objav o %s", - "More posts about": "Več objav o", "Newer posts": "Novejše objave", "Next post": "Naslednja objava", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Ni najdenih objav.", + "Nothing found.": "Brez zadetkov.", "Older posts": "Starejše objave", "Original site": "Izvorna spletna stran", - "Posted": "Objavljeno", "Posted:": "Objavljeno:", "Posts about %s": "Objave o %s", "Posts for year %s": "Objave za leto %s", "Posts for {month} {year}": "Objave za {month} {year}", "Previous post": "Prejšnja objava", + "Publication date": "Datum objave", + "RSS feed": "vir RSS", "Read in English": "Beri v slovenščini", "Read more": "Več o tem", "Source": "Izvor", diff --git a/nikola/data/themes/base/messages/messages_sl_si.py b/nikola/data/themes/base/messages/messages_sl_si.py deleted file mode 120000 index 152e151..0000000 --- a/nikola/data/themes/base/messages/messages_sl_si.py +++ /dev/null @@ -1 +0,0 @@ -messages_sl.py
\ No newline at end of file diff --git a/nikola/data/themes/base/messages/messages_tr.py b/nikola/data/themes/base/messages/messages_tr.py index ad92768..df9c4eb 120000..100644 --- a/nikola/data/themes/base/messages/messages_tr.py +++ b/nikola/data/themes/base/messages/messages_tr.py @@ -1 +1,33 @@ -messages_tr_tr.py
\ No newline at end of file +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "%d min remaining to read": "", + "Also available in:": "Şu dilde de mevcut:", + "Archive": "Arşiv", + "Categories": "Kategoriler", + "Comments": "Yorumlar", + "LANGUAGE": "Türkçe", + "Languages:": "Diller:", + "More posts about %s": "%s ilgili diğer yazılar", + "Newer posts": "Daha yeni yazılar", + "Next post": "Sonraki yazı", + "No posts found.": "Yazı bulunamadı.", + "Nothing found.": "Hiçbir şey bulunamadı.", + "Older posts": "Daha eski yazılar", + "Original site": "Orjinal web sayfası", + "Posted:": "Yayın tarihi:", + "Posts about %s": "%s ile ilgili yazılar", + "Posts for year %s": "%s yılındaki yazılar", + "Posts for {month} {year}": "{month} {year} göre yazılar", + "Previous post": "Önceki yazı", + "Publication date": "Yayınlanma tarihi", + "RSS feed": "RSS kaynağı", + "Read in English": "Türkçe olarak oku", + "Read more": "Devamını oku", + "Source": "Kaynak", + "Tags and Categories": "Etiketler ve Kategoriler", + "Tags": "Etiketler", + "old posts, page %d": "eski yazılar, sayfa %d", + "page %d": "sayfa %d", +} diff --git a/nikola/data/themes/base/messages/messages_tr_tr.py b/nikola/data/themes/base/messages/messages_tr_tr.py deleted file mode 100644 index 95c5736..0000000 --- a/nikola/data/themes/base/messages/messages_tr_tr.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- encoding:utf-8 -*- -from __future__ import unicode_literals - -MESSAGES = { - "Also available in": "Şu dilde de mevcut", - "Also available in:": "Şu dilde de mevcut:", - "Archive": "Arşiv", - "Categories": "Kategoriler", - "LANGUAGE": "Türkçe", - "More posts about %s": "%s ilgili diğer yazılar", - "More posts about": " ilgili diğer yazılar", - "Newer posts": "Daha yeni yazılar", - "Next post": "Sonraki yazı", - "No posts found.": "", - "Nothing found.": "", - "Older posts": "Daha eski yazılar", - "Original site": "Orjinal web sayfası", - "Posted": "Yayın tarihi", - "Posted:": "Yayın tarihi:", - "Posts about %s": "%s ile ilgili yazılar", - "Posts for year %s": "%s yılındaki yazılar", - "Posts for {month} {year}": "{month} {year} göre yazılar", - "Previous post": "Önceki yazı", - "Read in English": "Türkçe olarak oku", - "Read more": "Devamını oku", - "Source": "Kaynak", - "Tags and Categories": "Etiketler ve Kategoriler", - "Tags": "Etiketler", - "old posts, page %d": "eski yazılar, sayfa %d", - "page %d": "sayfa %d", -} diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py index 794861d..204d95f 100644 --- a/nikola/data/themes/base/messages/messages_ur.py +++ b/nikola/data/themes/base/messages/messages_ur.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "ان زبانوں میں بھی دستیاب", + "%d min remaining to read": "%d منٹ کا مطالعہ باقی", "Also available in:": "ان زبانوں میں بھی دستیاب:", "Archive": "آرکائیو", "Categories": "زمرے", + "Comments": "تبصرے", "LANGUAGE": "اردو", + "Languages:": "زبانیں:", "More posts about %s": "%s کے بارے میں مزید تحاریر", - "More posts about": " کے بارے میں مزید تحاریر", "Newer posts": "نئی تحاریر", "Next post": "اگلی تحریر", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "کوئی تحریر نہیں مل سکی۔", + "Nothing found.": "کچھ نہیں مل سکا۔", "Older posts": "پرانی تحاریر", "Original site": "اصلی سائٹ", - "Posted": "اشاعت", "Posted:": "اشاعت:", "Posts about %s": "%s کے بارے میں تحاریر", "Posts for year %s": "سال %s کی تحاریر", "Posts for {month} {year}": "{month} {year} کی تحاریر", "Previous post": "پچھلی تحریر", + "Publication date": "تاریخِ اشاعت", + "RSS feed": "آر ایس ایس فیڈ", "Read in English": "اردو میں پڑھیے", "Read more": "مزید پڑھیے", "Source": "سورس", diff --git a/nikola/data/themes/base/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py index 2f937c7..525cb45 100644 --- a/nikola/data/themes/base/messages/messages_zh_cn.py +++ b/nikola/data/themes/base/messages/messages_zh_cn.py @@ -2,25 +2,27 @@ from __future__ import unicode_literals MESSAGES = { - "Also available in": "其他语言版本", + "%d min remaining to read": "", "Also available in:": "其他语言版本:", "Archive": "文章存档", "Categories": "分类", + "Comments": "", "LANGUAGE": "简体中文", + "Languages:": "", "More posts about %s": "更多相关文章: %s", - "More posts about": "更多相关文章:", "Newer posts": "新一篇", "Next post": "后一篇", "No posts found.": "", "Nothing found.": "", "Older posts": "旧一篇", "Original site": "原文地址", - "Posted": "发表于", "Posted:": "发表于:", "Posts about %s": "文章分类:%s", "Posts for year %s": "%s年文章", "Posts for {month} {year}": "{year}年{month}月文章", "Previous post": "前一篇", + "Publication date": "", + "RSS feed": "", "Read in English": "中文版", "Read more": "更多", "Source": "源代码", diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl index 8a90349..f587593 100644 --- a/nikola/data/themes/base/templates/base.tmpl +++ b/nikola/data/themes/base/templates/base.tmpl @@ -1,45 +1,25 @@ ## -*- coding: utf-8 -*- <%namespace name="base" file="base_helper.tmpl" import="*"/> +<%namespace name="header" file="base_header.tmpl" import="*"/> +<%namespace name="footer" file="base_footer.tmpl" import="*"/> <%namespace name="annotations" file="annotation_helper.tmpl"/> ${set_locale(lang)} -<!DOCTYPE html> -<html -%if comment_system == 'facebook': -xmlns:fb="http://ogp.me/ns/fb#" -%endif -lang="${lang}"> -<head> - ${base.html_head()} - <%block name="extra_head"> - </%block> - ${extra_head_data} +${base.html_headstart()} +<%block name="extra_head"> +### Leave this block alone. +</%block> +${template_hooks['extra_head']()} </head> <body> - <h1 id="blog-title"> - <a href="${abs_link('/')}" title="${blog_title}" rel="home">${blog_title}</a> - </h1> - <%block name="belowtitle"> - %if len(translations) > 1: - <small> - ${messages("Also available in:")} - ${base.html_translations()} - </small> - %endif - </%block> - <%block name="content"></%block> - <small>${content_footer}</small> - <!--Sidebar content--> - <ul class="unstyled"> - %if license: - <li>${license} - %endif - ${base.html_social()} - ${base.html_navigation_links()} - %if search_form: - <li>${search_form} - %endif - </ul> + <div id="container"> + ${header.html_header()} + <main id="content"> + <%block name="content"></%block> + </main> + ${footer.html_footer()} + </div> + ${body_end} + ${template_hooks['body_end']()} ${base.late_load_js()} - ${social_buttons_code} </body> </html> diff --git a/nikola/data/themes/base/templates/base_footer.tmpl b/nikola/data/themes/base/templates/base_footer.tmpl new file mode 100644 index 0000000..9a1c00f --- /dev/null +++ b/nikola/data/themes/base/templates/base_footer.tmpl @@ -0,0 +1,11 @@ +## -*- coding: utf-8 -*- +<%namespace name="base" file="base_helper.tmpl" import="*"/> + +<%def name="html_footer()"> + %if content_footer: + <footer id="footer" role="contentinfo"> + <p>${content_footer}</p> + ${template_hooks['page_footer']()} + </footer> + %endif +</%def> diff --git a/nikola/data/themes/base/templates/base_header.tmpl b/nikola/data/themes/base/templates/base_header.tmpl new file mode 100644 index 0000000..0c6e12d --- /dev/null +++ b/nikola/data/themes/base/templates/base_header.tmpl @@ -0,0 +1,66 @@ +## -*- coding: utf-8 -*- +<%namespace name="base" file="base_helper.tmpl" import="*"/> + +<%def name="html_header()"> + <header id="header" role="banner"> + ${html_site_title()} + ${html_translation_header()} + ${html_navigation_links()} + %if search_form: + <div class="searchform" role="search"> + ${search_form} + </div> + %endif + </header> + ${template_hooks['page_header']()} +</%def> + +<%def name="html_site_title()"> + <h1 id="brand"><a href="${abs_link('/')}" title="${blog_title}" rel="home"> + %if logo_url: + <img src="${logo_url}" alt="${blog_title}" id="logo"> + %endif + + % if show_blog_title: + <span id="blog-title">${blog_title}</span> + % endif + </a></h1> +</%def> + +<%def name="html_navigation_links()"> + <nav id="menu" role="navigation"> + <ul> + %for url, text in navigation_links[lang]: + % if isinstance(url, tuple): + <li> ${text} + <ul> + %for suburl, text in url: + % if rel_link(permalink, suburl) == "#": + <li class="active"><a href="${permalink}">${text}</a></li> + %else: + <li><a href="${suburl}">${text}</a></li> + %endif + %endfor + </ul> + % else: + % if rel_link(permalink, url) == "#": + <li class="active"><a href="${permalink}">${text}</a></li> + %else: + <li><a href="${url}">${text}</a></li> + %endif + % endif + %endfor + ${template_hooks['menu']()} + ${template_hooks['menu_alt']()} + </ul> + </nav> +</%def> + +<%def name="html_translation_header()"> + %if len(translations) > 1: + <div id="toptranslations"> + <h2>${messages("Languages:")}</h2> + ${base.html_translations()} + </div> + %endif +</%def> diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl index 501c06e..beeff99 100644 --- a/nikola/data/themes/base/templates/base_helper.tmpl +++ b/nikola/data/themes/base/templates/base_helper.tmpl @@ -1,12 +1,67 @@ ## -*- coding: utf-8 -*- -<%def name="html_head()"> + +<%def name="html_headstart()"> +<!DOCTYPE html> +<html +\ +% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook'): +prefix='\ +%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']): +og: http://ogp.me/ns# \ +%endif +%if use_open_graph: +article: http://ogp.me/ns/article# \ +%endif +%if comment_system == 'facebook': +fb: http://ogp.me/ns/fb# \ +%endif +'\ +%endif +\ +% if is_rtl: +dir="rtl" \ +% endif +\ +lang="${lang}"> + <head> <meta charset="utf-8"> %if description: <meta name="description" content="${description}"> %endif - <meta name="author" content="${blog_author}"> + <meta name="viewport" content="width=device-width"> <title>${title|striphtml} | ${blog_title|striphtml}</title> + + ${html_stylesheets()} + ${html_feedlinks()} + %if permalink: + <link rel="canonical" href="${abs_link(permalink)}"> + %endif + + %if favicons: + %for name, file, size in favicons: + <link rel="${name}" href="${file}" sizes="${size}"/> + %endfor + %endif + + % if comment_system == 'facebook': + <meta property="fb:app_id" content="${comment_system_id}"> + % endif + ${mathjax_config} + %if use_cdn: + <!--[if lt IE 9]><script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + %else: + <!--[if lt IE 9]><script src="/assets/js/html5.js"></script><![endif]--> + %endif + + ${extra_head_data} +</%def> + +<%def name="late_load_js()"> + ${social_buttons_code} +</%def> + +<%def name="html_stylesheets()"> %if use_bundles: %if use_cdn: <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> @@ -16,20 +71,17 @@ %else: <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/theme.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"> %if has_custom_css: <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> %endif %endif - %if permalink: - <link rel="canonical" href="${abs_link(permalink)}"> - %endif - <!--[if lt IE 9]> - <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> - <![endif]--> +</%def> + +<%def name="html_feedlinks()"> %if rss_link: ${rss_link} - %else: + %elif generate_rss: %if len(translations) > 1: %for language in translations: <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}"> @@ -38,56 +90,14 @@ <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}"> %endif %endif - %if favicons: - %for name, file, size in favicons: - <link rel="${name}" href="${file}" sizes="${size}"/> - %endfor - %endif - % if comment_system == 'facebook': - <meta property="fb:app_id" content="${comment_system_id}"> - % endif </%def> -<%def name="late_load_js()"> -</%def> - -<%def name="html_social()"> - ${social_buttons_code} -</%def> - -<!--FIXME: remove in v7 --> -<%def name="html_sidebar_links()"> - ${html_navigation_links()} -</%def> - -<%def name="html_navigation_links()"> - %for url, text in navigation_links[lang]: - % if isinstance(url, tuple): - <li> ${text} - <ul> - %for suburl, text in url: - % if rel_link(permalink, suburl) == "#": - <li class="active"><a href="${permalink}">${text}</a> - %else: - <li><a href="${suburl}">${text}</a> - %endif - %endfor - </ul> - % else: - % if rel_link(permalink, url) == "#": - <li class="active"><a href="${permalink}">${text}</a> - %else: - <li><a href="${url}">${text}</a> - %endif - % endif - %endfor -</%def> - - <%def name="html_translations()"> + <ul class="translations"> %for langname in translations.keys(): %if langname != lang: - <a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a> + <li><a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li> %endif %endfor + </ul> </%def> diff --git a/nikola/data/themes/base/templates/comments_helper.tmpl b/nikola/data/themes/base/templates/comments_helper.tmpl index d3a2704..1459888 100644 --- a/nikola/data/themes/base/templates/comments_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper.tmpl @@ -1,12 +1,12 @@ ## -*- coding: utf-8 -*- -<%namespace name="disqus" file="disqus_helper.tmpl"/> -<%namespace name="livefyre" file="livefyre_helper.tmpl"/> -<%namespace name="intensedebate" file="intensedebate_helper.tmpl"/> -<%namespace name="moot" file="moot_helper.tmpl"/> -<%namespace name="googleplus" file="googleplus_helper.tmpl"/> -<%namespace name="facebook" file="facebook_helper.tmpl"/> -<%namespace name="isso" file="isso_helper.tmpl"/> +<%namespace name="disqus" file="comments_helper_disqus.tmpl"/> +<%namespace name="livefyre" file="comments_helper_livefyre.tmpl"/> +<%namespace name="intensedebate" file="comments_helper_intensedebate.tmpl"/> +<%namespace name="muut" file="comments_helper_muut.tmpl"/> +<%namespace name="googleplus" file="comments_helper_googleplus.tmpl"/> +<%namespace name="facebook" file="comments_helper_facebook.tmpl"/> +<%namespace name="isso" file="comments_helper_isso.tmpl"/> <%def name="comment_form(url, title, identifier)"> %if comment_system == 'disqus': @@ -15,8 +15,8 @@ ${livefyre.comment_form(url, title, identifier)} % elif comment_system == 'intensedebate': ${intensedebate.comment_form(url, title, identifier)} - % elif comment_system == 'moot': - ${moot.comment_form(url, title, identifier)} + % elif comment_system == 'muut': + ${muut.comment_form(url, title, identifier)} % elif comment_system == 'googleplus': ${googleplus.comment_form(url, title, identifier)} % elif comment_system == 'facebook': @@ -33,8 +33,8 @@ ${livefyre.comment_link(link, identifier)} % elif comment_system == 'intensedebate': ${intensedebate.comment_link(link, identifier)} - % elif comment_system == 'moot': - ${moot.comment_link(link, identifier)} + % elif comment_system == 'muut': + ${muut.comment_link(link, identifier)} % elif comment_system == 'googleplus': ${googleplus.comment_link(link, identifier)} % elif comment_system == 'facebook': @@ -51,8 +51,8 @@ ${livefyre.comment_link_script()} % elif comment_system == 'intensedebate': ${intensedebate.comment_link_script()} - % elif comment_system == 'moot': - ${moot.comment_link_script()} + % elif comment_system == 'muut': + ${muut.comment_link_script()} % elif comment_system == 'googleplus': ${googleplus.comment_link_script()} % elif comment_system == 'facebook': diff --git a/nikola/data/themes/base/templates/disqus_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl index 74187b0..8a94eaf 100644 --- a/nikola/data/themes/base/templates/disqus_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl @@ -25,15 +25,14 @@ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); </script> - <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript> - <a href="http://disqus.com" class="dsq-brlink" rel="nofollow">Comments powered by <span class="logo-disqus">Disqus</span></a> + <noscript>Please enable JavaScript to view the <a href="//disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript> + <a href="//disqus.com" class="dsq-brlink" rel="nofollow">Comments powered by <span class="logo-disqus">Disqus</span></a> %endif </%def> <%def name="comment_link(link, identifier)"> - <p> %if comment_system_id: - <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">Comments</a> + <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">Comments</a> %endif </%def> @@ -43,16 +42,3 @@ <script>var disqus_shortname="${comment_system_id}";(function(){var a=document.createElement("script");a.async=true;a.src="//"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(a)}());</script> %endif </%def> - -## FIXME: remove aliases in v7 -<%def name="html_disqus(url, title, identifier)"> - ${comment_form(url, title, identifier)} -</%def> - -<%def name="html_disqus_link(link, identifier)"> - ${comment_link(link, identifier)} -</%def> - -<%def name="html_disqus_script()"> - ${comment_link_script()} -</%def> diff --git a/nikola/data/themes/base/templates/facebook_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_facebook.tmpl index d6059a1..d6059a1 100644 --- a/nikola/data/themes/base/templates/facebook_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_facebook.tmpl diff --git a/nikola/data/themes/base/templates/googleplus_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl index 5a5c4d7..5a5c4d7 100644 --- a/nikola/data/themes/base/templates/googleplus_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl diff --git a/nikola/data/themes/base/templates/intensedebate_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl index 6462f74..c47b6c7 100644 --- a/nikola/data/themes/base/templates/intensedebate_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl @@ -6,7 +6,7 @@ var idcomments_post_id = "${identifier}"; var idcomments_post_url = "${url}"; </script> <span id="IDCommentsPostTitle" style="display:none"></span> -<script type='text/javascript' src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script> +<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script> </script> </%def> @@ -17,7 +17,7 @@ var idcomments_acct = '${comment_system_id}'; var idcomments_post_id = "${identifier}"; var idcomments_post_url = "${link}"; </script> -<script type="text/javascript" src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> +<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> </a> </%def> diff --git a/nikola/data/themes/base/templates/isso_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_isso.tmpl index 8dc95f5..8dc95f5 100644 --- a/nikola/data/themes/base/templates/isso_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_isso.tmpl diff --git a/nikola/data/themes/base/templates/livefyre_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl index 6916459..68d99e5 100644 --- a/nikola/data/themes/base/templates/livefyre_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl @@ -1,8 +1,8 @@ ## -*- coding: utf-8 -*- <%def name="comment_form(url, title, identifier)"> <div id="livefyre-comments"></div> -<script type="text/javascript" src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script> -<script type="text/javascript"> +<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script> +<script> (function () { var articleId = "${identifier}"; fyre.conv.load({}, [{ @@ -21,17 +21,13 @@ </%def> <%def name="comment_link(link, identifier)"> - <p> <a href="${link}"> <span class="livefyre-commentcount" data-lf-site-id="${comment_system_id}" data-lf-article-id="${identifier}"> 0 Comments - </span></a> + </span> </%def> <%def name="comment_link_script()"> -<script - type="text/javascript" - src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"> -</script> +<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script> </%def> diff --git a/nikola/data/themes/base/templates/mustache-comment-form.tmpl b/nikola/data/themes/base/templates/comments_helper_mustache.tmpl index 593d0aa..593d0aa 100644 --- a/nikola/data/themes/base/templates/mustache-comment-form.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_mustache.tmpl diff --git a/nikola/data/themes/base/templates/moot_helper.tmpl b/nikola/data/themes/base/templates/comments_helper_muut.tmpl index 951553e..94532d9 100644 --- a/nikola/data/themes/base/templates/moot_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_muut.tmpl @@ -1,7 +1,7 @@ ## -*- coding: utf-8 -*- <%def name="comment_form(url, title, identifier)"> - <a class="moot" href="https://moot.it/i/${comment_system_id}/${identifier}">${comment_system_id} forums</a> + <a class="muut" href="https://muut.com/i/${comment_system_id}/${identifier}">${comment_system_id} forums</a> </%def> <%def name="comment_link(link, identifier)"> @@ -9,5 +9,5 @@ <%def name="comment_link_script()"> -<script src="https://cdn.moot.it/1/moot.min.js"></script> +<script src="//cdn.muut.com/1/moot.min.js"></script> </%def> diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/crumbs.tmpl index 8fbafcf..de8e570 100644 --- a/nikola/data/themes/base/templates/crumbs.tmpl +++ b/nikola/data/themes/base/templates/crumbs.tmpl @@ -2,10 +2,12 @@ <%def name="bar(crumbs)"> %if crumbs: +<nav class="breadcrumbs"> <ul class="breadcrumb"> % for link, text in crumbs: <li><a href="${link}">${text}</a></li> % endfor </ul> +</nav> %endif </%def> diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl index 731a75a..ca9da05 100644 --- a/nikola/data/themes/base/templates/gallery.tmpl +++ b/nikola/data/themes/base/templates/gallery.tmpl @@ -9,9 +9,9 @@ %if title: <h1>${title}</h1> %endif - %if text: + %if post: <p> - ${text} + ${post.text()} </p> %endif %if folders: @@ -30,7 +30,7 @@ %endfor </ul> %endif -%if enable_comments: +%if site_has_comments and enable_comments: ${comments.comment_form(None, permalink, title)} %endif </%block> diff --git a/nikola/data/themes/base/templates/index.tmpl b/nikola/data/themes/base/templates/index.tmpl index 1a280b0..e833eb0 100644 --- a/nikola/data/themes/base/templates/index.tmpl +++ b/nikola/data/themes/base/templates/index.tmpl @@ -2,28 +2,33 @@ <%namespace name="helper" file="index_helper.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> <%inherit file="base.tmpl"/> + <%block name="content"> - % for post in posts: - <article class="postbox h-entry post-${post.meta('type')}"> - <h1 class="p-name"><a href="${post.permalink()}" class="u-url">${post.title()}</a> - <small> - ${messages("Posted:")} <time class="published dt-published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time> - </small></h1> - <hr> - %if index_teasers: - <div class="p-summary"> - ${post.text(teaser_only=True)} - %else: - <div class="e-content"> - ${post.text(teaser_only=False)} - %endif +<div class="postindex"> +% for post in posts: + <article class="h-entry post-${post.meta('type')}"> + <header> + <h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()}</h1></a> + <div class="metadata"> + <p class="byline author vcard"><span class="byline-name fn">${post.author()}</span></p> + <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></a></p> + % if not post.meta('nocomments') and site_has_comments: + <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)} + % endif </div> - % if not post.meta('nocomments'): - ${comments.comment_link(post.permalink(), post._base_path)} - % endif - </article> - % endfor - ${helper.html_pager()} - ${comments.comment_link_script()} - ${helper.mathjax_script(posts)} + </header> + %if index_teasers: + <div class="p-summary entry-summary"> + ${post.text(teaser_only=True)} + %else: + <div class="e-content entry-content"> + ${post.text(teaser_only=False)} + %endif + </div> + </article> +% endfor +</div> +${helper.html_pager()} +${comments.comment_link_script()} +${helper.mathjax_script(posts)} </%block> diff --git a/nikola/data/themes/base/templates/index_helper.tmpl b/nikola/data/themes/base/templates/index_helper.tmpl index c925559..9331b93 100644 --- a/nikola/data/themes/base/templates/index_helper.tmpl +++ b/nikola/data/themes/base/templates/index_helper.tmpl @@ -1,30 +1,27 @@ ## -*- coding: utf-8 -*- <%def name="html_pager()"> %if prevlink or nextlink: - <div> + <nav class="postindexpager"> <ul class="pager"> %if prevlink: <li class="previous"> - <a href="${prevlink}" rel="prev">← ${messages("Newer posts")}</a> + <a href="${prevlink}" rel="prev">${messages("Newer posts")}</a> </li> %endif %if nextlink: <li class="next"> - <a href="${nextlink}" rel="next">${messages("Older posts")} →</a> + <a href="${nextlink}" rel="next">${messages("Older posts")}</a> </li> %endif </ul> - </div> + </nav> %endif </%def> <%def name="mathjax_script(posts)"> %if any(post.is_mathjax for post in posts): <script type="text/x-mathjax-config"> - MathJax.Hub.Config({ - tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]} - }); - </script> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});</script> <script src="/assets/js/mathjax.js"></script> %endif </%def> diff --git a/nikola/data/themes/base/templates/list.tmpl b/nikola/data/themes/base/templates/list.tmpl index 4136eb9..4082516 100644 --- a/nikola/data/themes/base/templates/list.tmpl +++ b/nikola/data/themes/base/templates/list.tmpl @@ -1,16 +1,19 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> + <%block name="content"> - <!--Body content--> - <div class="postbox"> +<article class="listpage"> + <header> <h1>${title}</h1> - %if items: - <ul class="unstyled"> - % for text, link in items: - <li><a href="${link}">${text}</a> - % endfor - </ul> - %endif - </div> - <!--End of body content--> + </header> + %if items: + <ul class="postlist"> + % for text, link in items: + <li><a href="${link}">${text}</a> + % endfor + </ul> + %else: + <p>${messages("Nothing found.")}</p> + %endif +</article> </%block> diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl index b27f230..0ef164f 100644 --- a/nikola/data/themes/base/templates/list_post.tmpl +++ b/nikola/data/themes/base/templates/list_post.tmpl @@ -1,16 +1,19 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> + <%block name="content"> - <!--Body content--> - <div class="postbox"> +<article class="listpage"> + <header> <h1>${title}</h1> - %if posts: - <ul class="unstyled"> - % for post in posts: - <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a> - % endfor - </ul> - %endif - </div> - <!--End of body content--> + </header> + %if posts: + <ul class="postlist"> + % for post in posts: + <li><a href="${post.permalink()}" class="listtitle">${post.title()}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></li> + % endfor + </ul> + %else: + <p>${messages("No posts found.")}</p> + %endif +</article> </%block> diff --git a/nikola/data/themes/base/templates/listing.tmpl b/nikola/data/themes/base/templates/listing.tmpl index 0662360..e0bf91b 100644 --- a/nikola/data/themes/base/templates/listing.tmpl +++ b/nikola/data/themes/base/templates/listing.tmpl @@ -4,7 +4,7 @@ <%block name="content"> ${ui.bar(crumbs)} %if folders or files: -<ul class="unstyled"> +<ul> % for name in folders: <li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a> % endfor @@ -16,4 +16,8 @@ ${ui.bar(crumbs)} % if code: ${code} % endif +% if source_link: + <p class="sourceline"><a href="${source_link}" id="sourcelink">${messages("Source")}</a></p> +% endif </%block> + diff --git a/nikola/data/themes/base/templates/post.tmpl b/nikola/data/themes/base/templates/post.tmpl index 981fd97..0babb2b 100644 --- a/nikola/data/themes/base/templates/post.tmpl +++ b/nikola/data/themes/base/templates/post.tmpl @@ -1,42 +1,39 @@ ## -*- coding: utf-8 -*- <%namespace name="helper" file="post_helper.tmpl"/> +<%namespace name="pheader" file="post_header.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> <%inherit file="base.tmpl"/> + <%block name="extra_head"> -${helper.twitter_card_information(post)} -% if post.meta('keywords'): - <meta name="keywords" content="${post.meta('keywords')|h}"> -% endif -${helper.meta_translations(post)} + ${parent.extra_head()} + % if post.meta('keywords'): + <meta name="keywords" content="${post.meta('keywords')|h}"> + % endif + <meta name="author" content="${post.author()}"> + ${helper.open_graph_metadata(post)} + ${helper.twitter_card_information(post)} + ${helper.meta_translations(post)} </%block> + <%block name="content"> - <article class="postbox post-${post.meta('type')}"> - <div class="h-entry" itemscope="itemscope" itemtype="http://schema.org/Article"> - ${helper.html_title()} - <hr> - <small> - ${messages("Posted:")} <time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished">${post.formatted_date(date_format)}</time> - ${helper.html_translations(post)} - ${helper.html_tags(post)} - | - <%block name="sourcelink"> - % if not post.meta('password'): - <a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a> - % endif - </%block> - </small> - <hr> - <div class="e-content" itemprop="articleBody text"> +<article class="post-${post.meta('type')} h-entry hentry postpage" itemscope="itemscope" itemtype="http://schema.org/Article"> + ${pheader.html_post_header()} + <div class="e-content entry-content" itemprop="articleBody text"> ${post.text()} </div> - %if post.description(): - <meta content="${post.description()}" itemprop="description"> - %endif - </div> + <aside class="postpromonav"> + <nav> + ${helper.html_tags(post)} ${helper.html_pager(post)} - % if not post.meta('nocomments'): + </nav> + </aside> + % if not post.meta('nocomments') and site_has_comments: + <section class="comments"> + <h2>${messages("Comments")}</h2> ${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)} + </section> % endif ${helper.mathjax_script(post)} - </article> +</article> +${comments.comment_link_script()} </%block> diff --git a/nikola/data/themes/base/templates/post_header.tmpl b/nikola/data/themes/base/templates/post_header.tmpl new file mode 100644 index 0000000..c848186 --- /dev/null +++ b/nikola/data/themes/base/templates/post_header.tmpl @@ -0,0 +1,49 @@ +## -*- coding: utf-8 -*- +<%namespace name="helper" file="post_helper.tmpl"/> +<%namespace name="comments" file="comments_helper.tmpl"/> + +<%def name="html_title()"> +%if title and not post.meta('hidetitle'): + <h1 class="p-name entry-title" itemprop="headline name"><a href="${post.permalink()}" class="u-url">${title|h}</a></h1> +%endif +</%def> + +<%def name="html_translations(post)"> + % if len(translations) > 1: + <div class="metadata posttranslations translations"> + <h3 class="posttranslations-intro">${messages("Also available in:")}</h3> + % for langname in translations.keys(): + % if langname != lang and post.is_translation_available(langname): + <p><a href="${post.permalink(langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></p> + % endif + % endfor + </div> + % endif +</%def> + +<%def name="html_sourcelink()"> + % if show_sourcelink: + <p class="sourceline"><a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a></p> + % endif +</%def> + +<%def name="html_post_header()"> + <header> + ${html_title()} + <div class="metadata"> + <p class="byline author vcard"><span class="byline-name fn">${post.author()}</span></p> + <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></a></p> + % if not post.meta('nocomments') and site_has_comments: + <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)} + % endif + ${html_sourcelink()} + % if post.meta('link'): + <p><a href='${post.meta('link')}'>${messages("Original site")}</a></p> + % endif + %if post.description(): + <meta name="description" itemprop="description" content="${post.description()}"> + %endif + </div> + ${html_translations(post)} + </header> +</%def> diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl index 391350d..85ba378 100644 --- a/nikola/data/themes/base/templates/post_helper.tmpl +++ b/nikola/data/themes/base/templates/post_helper.tmpl @@ -1,22 +1,4 @@ ## -*- coding: utf-8 -*- -<%def name="html_title()"> - <h1 class="p-name" itemprop="headline name">${title|h}</h1> - % if link: - <p><a href='${link}'>${messages("Original site")}</a></p> - % endif -</%def> - - -<%def name="html_translations(post)"> - %if len(translations) > 1: - %for langname in translations.keys(): - %if langname != lang and post.is_translation_available(langname): - | - <a href="${post.permalink(langname)}" rel="alternate" hreflang="${langname}">${messages("Read in English", langname)}</a> - %endif - %endfor - %endif -</%def> <%def name="meta_translations(post)"> %if len(translations) > 1: @@ -28,18 +10,13 @@ %endif </%def> -<%def name="html_list_tags(post)" buffered="True"> - <span itemprop="keywords"> - %for tag in post.tags: - <a class="tag p-category" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a> - %endfor - </span> -</%def> - <%def name="html_tags(post)"> %if post.tags: - | - ${formatmsg(messages("More posts about %s"), html_list_tags(post))} + <ul itemprop="keywords" class="tags"> + %for tag in post.tags: + <li><a class="tag p-category" href="${_link('tag', tag)}" rel="tag">${tag}</a></li> + %endfor + </ul> %endif </%def> @@ -48,22 +25,35 @@ <ul class="pager"> %if post.prev_post: <li class="previous"> - <a href="${post.prev_post.permalink()}" rel="prev">← ${messages("Previous post")}</a> + <a href="${post.prev_post.permalink()}" rel="prev" title="${post.prev_post.title()}">${messages("Previous post")}</a> </li> %endif %if post.next_post: <li class="next"> - <a href="${post.next_post.permalink()}" rel="next">${messages("Next post")} →</a> + <a href="${post.next_post.permalink()}" rel="next" title="${post.next_post.title()}">${messages("Next post")}</a> </li> %endif </ul> %endif </%def> +<%def name="open_graph_metadata(post)"> + %if use_open_graph: + <meta name="og:title" content="${post.title()[:70]|h}"> + <meta name="og:url" content="${abs_link(permalink)}"> + %if post.description(): + <meta name="og:description" content="${post.description()[:200]|h}"> + %else: + <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}"> + %endif + <meta name="og:site_name" content="${blog_title|striphtml}"> + <meta name="og:type" content="article"> + %endif +</%def> + <%def name="twitter_card_information(post)"> %if twitter_card and twitter_card['use_twitter_cards']: <meta name="twitter:card" content="${twitter_card.get('card', 'summary')|h}"> - <meta name="og:url" content="${post.permalink(absolute=True)}"> %if 'site:id' in twitter_card: <meta name="twitter:site:id" content="${twitter_card['site:id']}"> %elif 'site' in twitter_card: @@ -74,22 +64,13 @@ %elif 'creator' in twitter_card: <meta name="twitter:creator" content="${twitter_card['creator']}"> %endif - <meta name="og:title" content="${post.title()[:70]|h}"> - %if post.description(): - <meta name="og:description" content="${post.description()[:200]|h}"> - %else: - <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}"> - %endif %endif </%def> <%def name="mathjax_script(post)"> %if post.is_mathjax: <script type="text/x-mathjax-config"> - MathJax.Hub.Config({ - tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]} - }); - </script> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});</script> <script src="/assets/js/mathjax.js"></script> %endif </%def> diff --git a/nikola/data/themes/base/templates/post_list_directive.tmpl b/nikola/data/themes/base/templates/post_list_directive.tmpl index b31d242..d9166e9 100644 --- a/nikola/data/themes/base/templates/post_list_directive.tmpl +++ b/nikola/data/themes/base/templates/post_list_directive.tmpl @@ -1,4 +1,5 @@ ## -*- coding: utf-8 -*- +<%block name="content"> <!-- Begin post-list ${post_list_id} --> <div id="${post_list_id}" class="post-list"> %if posts: @@ -14,3 +15,4 @@ %endif </div> <!-- End post-list ${post_list_id} --> +</%block> diff --git a/nikola/data/themes/base/templates/slides.tmpl b/nikola/data/themes/base/templates/slides.tmpl index 14983ad..048fb7e 100644 --- a/nikola/data/themes/base/templates/slides.tmpl +++ b/nikola/data/themes/base/templates/slides.tmpl @@ -1,6 +1,7 @@ +<%block name="content"> <div id="${carousel_id}" class="carousel slide"> <ol class="carousel-indicators"> - % for i in range(len(content)): + % for i in range(len(slides_content)): % if i == 0: <li data-target="#${carousel_id}" data-slide-to="${i}" class="active"></li> % else: @@ -9,7 +10,7 @@ % endfor </ol> <div class="carousel-inner"> - % for i, image in enumerate(content): + % for i, image in enumerate(slides_content): % if i == 0: <div class="item active"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div> % else: @@ -20,3 +21,4 @@ <a class="left carousel-control" href="#${carousel_id}" data-slide="prev">‹</a> <a class="right carousel-control" href="#${carousel_id}" data-slide="next">›</a> </div> +</%block> diff --git a/nikola/data/themes/base/templates/story.tmpl b/nikola/data/themes/base/templates/story.tmpl index 7406f05..e3e3054 100644 --- a/nikola/data/themes/base/templates/story.tmpl +++ b/nikola/data/themes/base/templates/story.tmpl @@ -1,16 +1,37 @@ ## -*- coding: utf-8 -*- -<%inherit file="post.tmpl"/> <%namespace name="helper" file="post_helper.tmpl"/> +<%namespace name="pheader" file="post_header.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> +<%inherit file="post.tmpl"/> + <%block name="extra_head"> -${helper.twitter_card_information(post)} + ${parent.extra_head()} + % if post.meta('keywords'): + <meta name="keywords" content="${post.meta('keywords')|h}"> + % endif + <meta name="author" content="${post.author()}"> + ${helper.open_graph_metadata(post)} + ${helper.twitter_card_information(post)} + ${helper.meta_translations(post)} + %if post.description(): + <meta name="description" itemprop="description" content="${post.description()}"> + %endif </%block> + <%block name="content"> -%if title and not post.meta('hidetitle'): - <h1>${title}</h1> -%endif +<article class="storypage" itemscope="itemscope" itemtype="http://schema.org/Article"> + <header> + ${pheader.html_title()} + ${pheader.html_translations(post)} + </header> + <div itemprop="articleBody text"> ${post.text()} -%if enable_comments and not post.meta('nocomments'): - ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)} -%endif + </div> + %if site_has_comments and enable_comments and not post.meta('nocomments'): + <section class="comments"> + <h2>${messages("Comments")}</h2> + ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)} + </section> + %endif +</article> </%block> diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl index 43afd54..bff82c2 100644 --- a/nikola/data/themes/base/templates/tag.tmpl +++ b/nikola/data/themes/base/templates/tag.tmpl @@ -1,34 +1,40 @@ ## -*- coding: utf-8 -*- <%inherit file="list_post.tmpl"/> + <%block name="extra_head"> - %if len(translations) > 1: + ${parent.extra_head()} + %if len(translations) > 1 and generate_rss: %for language in translations: <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for ${kind} ${tag} (${language})" href="${_link(kind + "_rss", tag, language)}"> %endfor - %else: + %elif generate_rss: <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for ${kind} ${tag}" href="${_link(kind + "_rss", tag)}"> %endif </%block> + <%block name="content"> - <!--Body content--> - <div class="postbox"> +<article class="tagpage"> + <header> <h1>${title}</h1> - %if len(translations) > 1: - %for language in translations: - <a href="${_link(kind + "_rss", tag, language)}">RSS (${language})</a> - %endfor - %else: - <a href="${_link(kind + "_rss", tag)}">RSS</a> - %endif - <br> - %if posts: - <ul class="unstyled"> - % for post in posts: - <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a> - % endfor - </ul> - %endif + <div class="metadata"> + %if len(translations) > 1 and generate_rss: + %for language in translations: + <p class="feedlink"> + <a href="${_link(kind + "_rss", tag, language)}" hreflang="${language}" type="application/rss+xml">${messages('RSS feed', language)} (${language})</a> + </p> + %endfor + %elif generate_rss: + <p class="feedlink"><a href="${_link(kind + "_rss", tag)}" type="application/rss+xml">${messages('RSS feed')}</a></p> + %endif </div> - <!--End of body content--> + </header> + %if posts: + <ul class="postlist"> + % for post in posts: + <li><a href="${post.permalink()}" class="listtitle">${post.title()}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></li> + % endfor + </ul> + %endif +</article> </%block> diff --git a/nikola/data/themes/base/templates/tags.tmpl b/nikola/data/themes/base/templates/tags.tmpl index 6c8c5e9..3e0c4b4 100644 --- a/nikola/data/themes/base/templates/tags.tmpl +++ b/nikola/data/themes/base/templates/tags.tmpl @@ -1,10 +1,14 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> + <%block name="content"> - <h1>${title}</h1> +<article class="tagindex"> + <header> + <h1>${title}</h1> + </header> % if cat_items: <h2>${messages("Categories")}</h2> - <ul class="unstyled bricks"> + <ul class="postlist"> % for text, link in cat_items: % if text: <li><a class="reference" href="${link}">${text}</a></li> @@ -16,10 +20,11 @@ % endif %endif % if items: - <ul class="unstyled bricks"> + <ul class="postlist"> % for text, link in items: - <li><a class="reference" href="${link}">${text}</a></li> + <li><a class="reference listtitle" href="${link}">${text}</a></li> % endfor </ul> % endif +</article> </%block> diff --git a/nikola/data/themes/bootstrap-jinja/AUTHORS.txt b/nikola/data/themes/bootstrap-jinja/AUTHORS.txt new file mode 100644 index 0000000..043d497 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/AUTHORS.txt @@ -0,0 +1 @@ +Roberto Alsina <https://github.com/ralsina> diff --git a/nikola/data/themes/bootstrap-jinja/README.md b/nikola/data/themes/bootstrap-jinja/README.md new file mode 100644 index 0000000..5340fe2 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/README.md @@ -0,0 +1,23 @@ +A "website-done-with-bootstrap" theme, so to speak. + +Has a fixed navigation bar at top that displays the NAVIGATION_LINKS +setting and supports nested menus. + +This theme is used in Nikola's website: http://getnikola.com + +Important: To fit in the bootstrap navigation bar, the search form needs the +navbar-form and pull-left CSS classes applied. Here is an example with Nikola's +default duckduckgo search form: + + SEARCH_FORM = """ + <!-- Custom search --> + <form method="get" id="search" action="http://duckduckgo.com/" class="navbar-form pull-left"> + <input type="hidden" name="sites" value="%s"/> + <input type="hidden" name="k8" value="#444444"/> + <input type="hidden" name="k9" value="#D51920"/> + <input type="hidden" name="kt" value="h"/> + <input type="text" name="q" maxlength="255" placeholder="Search…" class="span2" style="margin-top: 4px;"/> + <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> + </form> + <!-- End of custom search --> + """ % SITE_URL diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css b/nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css new file mode 120000 index 0000000..5f8b3b0 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css @@ -0,0 +1 @@ +../../../../../../bower_components/jquery-colorbox/example3/colorbox.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png b/nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png new file mode 120000 index 0000000..841a726 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/example3/images/controls.png
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif b/nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif new file mode 120000 index 0000000..b192a75 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/example3/images/loading.gif
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap-jinja/assets/css/theme.css new file mode 120000 index 0000000..7566a80 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/css/theme.css @@ -0,0 +1 @@ +../../../bootstrap/assets/css/theme.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js new file mode 120000 index 0000000..f83073f --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js new file mode 120000 index 0000000..bafc4e0 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js new file mode 120000 index 0000000..a749232 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js new file mode 120000 index 0000000..e4a595c --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js new file mode 120000 index 0000000..1e9a1d6 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js new file mode 120000 index 0000000..748f53b --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js new file mode 120000 index 0000000..1154fb5 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js new file mode 120000 index 0000000..483e192 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js new file mode 120000 index 0000000..a30b13c --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js new file mode 120000 index 0000000..2a7e8ad --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js new file mode 120000 index 0000000..e359290 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js new file mode 120000 index 0000000..04fa276 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js new file mode 120000 index 0000000..d8105ab --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js new file mode 120000 index 0000000..72dddf5 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js new file mode 120000 index 0000000..34aa3c0 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js new file mode 120000 index 0000000..a87f03c --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js new file mode 120000 index 0000000..31053b8 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js new file mode 120000 index 0000000..aad9d22 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js new file mode 120000 index 0000000..3ea27c2 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js new file mode 120000 index 0000000..3e23b4a --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js new file mode 120000 index 0000000..374b9bb --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js new file mode 120000 index 0000000..101b476 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js new file mode 120000 index 0000000..8e14f15 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js new file mode 120000 index 0000000..2d03d48 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js new file mode 120000 index 0000000..9af0ba7 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js new file mode 120000 index 0000000..34f8ab1 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js new file mode 120000 index 0000000..76f289e --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-br.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js new file mode 120000 index 0000000..555f2e6 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js new file mode 120000 index 0000000..bac4855 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js new file mode 120000 index 0000000..65b0492 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js new file mode 120000 index 0000000..99859fd --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js new file mode 120000 index 0000000..c4fd9d5 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js new file mode 120000 index 0000000..d7f26e0 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js new file mode 120000 index 0000000..86fd98f --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js new file mode 120000 index 0000000..7cd1336 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js new file mode 120000 index 0000000..e6c5965 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js new file mode 120000 index 0000000..bd2254c --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js new file mode 120000 index 0000000..c195756 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js @@ -0,0 +1 @@ +../../../bootstrap/assets/js/flowr.plugin.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js new file mode 120000 index 0000000..5ee7a90 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js @@ -0,0 +1 @@ +../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/bundles b/nikola/data/themes/bootstrap-jinja/bundles new file mode 120000 index 0000000..3e517bb --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/bundles @@ -0,0 +1 @@ +../bootstrap/bundles
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/engine b/nikola/data/themes/bootstrap-jinja/engine new file mode 100644 index 0000000..6f04b30 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/engine @@ -0,0 +1 @@ +jinja diff --git a/nikola/data/themes/bootstrap-jinja/parent b/nikola/data/themes/bootstrap-jinja/parent new file mode 100644 index 0000000..e9ed660 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/parent @@ -0,0 +1 @@ +base-jinja diff --git a/nikola/data/themes/bootstrap-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap-jinja/templates/base.tmpl new file mode 100644 index 0000000..a433721 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/base.tmpl @@ -0,0 +1,86 @@ +{# -*- coding: utf-8 -*- #} +{% import 'base_helper.tmpl' as base with context %} +{% import 'annotation_helper.tmpl' as notes with context %} +{{ set_locale(lang) }} +{{ base.html_headstart() }} +{% block extra_head %} +{# Leave this block alone. #} +{% endblock %} +{{ template_hooks['extra_head']() }} +</head> +<body> + +<!-- Menubar --> + +<div class="navbar navbar-fixed-top" id="navbar"> + <div class="navbar-inner"> + <div class="container"> + + <!-- .btn-navbar is used as the toggle for collapsed navbar content --> + <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </a> + + <a class="brand" href="{{ abs_link('/') }}"> + {% if logo_url %} + <img src="{{ logo_url }}" alt="{{ blog_title }}" id="logo"> + {% endif %} + + {% if show_blog_title %} + <span id="blog-title">{{ blog_title }}</span> + {% endif %} + </a> + <!-- Everything you want hidden at 940px or less, place within here --> + <div class="nav-collapse collapse"> + <ul class="nav"> + {{ base.html_navigation_links() }} + {{ template_hooks['menu']() }} + </ul> + {% if search_form %} + {{ search_form }} + {% endif %} + <ul class="nav pull-right"> + {% block belowtitle %} + {% if translations|length > 1 %} + <li>{{ base.html_translations() }}</li> + {% endif %} + {% endblock %} + {% if show_sourcelink %} + <li>{% block sourcelink %}{% endblock %}</li> + {% endif %} + {{ template_hooks['menu_alt']() }} + </ul> + </div> + </div> + </div> +</div> +<!-- End of Menubar --> +<div class="container-fluid" id="container-fluid"> + <!--Body content--> + <div class="row-fluid"> + <div class="span2"></div> + <div class="span8"> + {{ template_hooks['page_header']() }} + {% block content %}{% endblock %} + </div> + </div> + <!--End of body content--> +</div> +<div class="footerbox"> + {{ content_footer }} + {{ template_hooks['page_footer']() }} +</div> +{{ base.late_load_js() }} + <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script> + {% block extra_js %}{% endblock %} + {% if annotations and post and not post.meta('noannotations') %} + {{ notes.code() }} + {% elif not annotations and post and post.meta('annotations') %} + {{ notes.code() }} + {% endif %} +{{ body_end }} +{{ template_hooks['body_end']() }} +</body> +</html> diff --git a/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl new file mode 100644 index 0000000..d8398b8 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl @@ -0,0 +1,161 @@ +{# -*- coding: utf-8 -*- #} + +{% macro html_headstart() %} +<!DOCTYPE html> +<html + +{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook') %} +prefix=' +{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %} +og: http://ogp.me/ns# +{% endif %} +{% if use_open_graph %} +article: http://ogp.me/ns/article# +{% endif %} +{% if comment_system == 'facebook' %} +fb: http://ogp.me/ns/fb# +{% endif %} +' +{% endif %} + +{% if is_rtl %} +dir="rtl" +{% endif %} + +lang="{{ lang }}"> + <head> + <meta charset="utf-8"> + {% if description %} + <meta name="description" content="{{ description }}"> + {% endif %} + <meta name="viewport" content="width=device-width"> + <title>{{ title|e }} | {{ blog_title|e }}</title> + + {{ html_stylesheets() }} + {{ html_feedlinks() }} + {% if permalink %} + <link rel="canonical" href="{{ abs_link(permalink) }}"> + {% endif %} + + {% if favicons %} + {% for name, file, size in favicons %} + <link rel="{{ name }}" href="{{ file }}" sizes="{{ size }}"/> + {% endfor %} + {% endif %} + + {% if comment_system == 'facebook' %} + <meta property="fb:app_id" content="{{ comment_system_id }}"> + {% endif %} + + {{ mathjax_config }} + {% if use_cdn %} + <!--[if lt IE 9]><script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + {% else %} + <!--[if lt IE 9]><script src="/assets/js/html5.js"></script><![endif]--> + {% endif %} + + {{ extra_head_data }} +{% endmacro %} + + +{% macro late_load_js() %} + {% if use_bundles %} + {% if use_cdn %} + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script> + <script src="/assets/js/all.js"></script> + {% else %} + <script src="/assets/js/all-nocdn.js"></script> + {% endif %} + {% else %} + {% if use_cdn %} + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script> + {% else %} + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + {% endif %} + <script src="/assets/js/jquery.colorbox-min.js"></script> + {% endif %} + {% if colorbox_locales[lang] %} + <script src="/assets/js/colorbox-i18n/jquery.colorbox-{{ colorbox_locales[lang] }}.js"></script> + {% endif %} + {{ social_buttons_code }} +{% endmacro %} + + +{% macro html_stylesheets() %} + {% if use_bundles %} + {% if use_cdn %} + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet"> + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + {% else %} + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + {% endif %} + {% else %} + {% if use_cdn %} + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet"> + {% else %} + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css"> + {% endif %} + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"> + {% if has_custom_css %} + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + {% endif %} + {% endif %} + {% if annotations and post and not post.meta('noannotations') %} + {{ notes.css() }} + {% elif not annotations and post and post.meta('annotations') %} + {{ notes.css() }} + {% endif %} +{% endmacro %} + + +{% macro html_navigation_links() %} + {% for url, text in navigation_links[lang] %} + {% if url is mapping %} + <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }}<b class="caret"></b></a> + <ul class="dropdown-menu"> + {% for suburl, text in url %} + {% if rel_link(permalink, suburl) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a> + {% else %} + <li><a href="{{ suburl }}">{{ text }}</a> + {% endif %} + {% endfor %} + </ul> + {% else %} + {% if rel_link(permalink, url) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a> + {% else %} + <li><a href="{{ url }}">{{ text }}</a> + {% endif %} + {% endif %} + {% endfor %} +{% endmacro %} + +{% macro html_feedlinks() %} + {% if rss_link %} + {{ rss_link }} + {% elif generate_rss %} + {% if translations|length > 1 %} + {% for language in translations %} + <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}"> + {% endfor %} + {% else %} + <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}"> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro html_translations() %} + {% for langname in translations.keys() %} + {% if langname != lang %} + <li><a href="{{ _link("index", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl b/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl new file mode 100644 index 0000000..e426774 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl @@ -0,0 +1,78 @@ +{# -*- coding: utf-8 -*- #} +{# Override only the functions that differ from base_helper.tmpl #} + +{% block html_stylesheets %} + {% if use_bundles %} + {% if use_cdn %} + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet"> + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + {% else %} + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + {% endif %} + {% else %} + {% if use_cdn %} + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet"> + {% else %} + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css"> + {% endif %} + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> + {% if has_custom_css %} + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + {% endif %} + {% endif %} + {% if annotations and post and not post.meta('noannotations') %} + {{ notes.css() }} + {% elif not annotations and post and post.meta('annotations') %} + {{ notes.css() }} + {% endif %} +{% endblock %} + + +{% block late_load_js %} + {% if use_bundles %} + {% if use_cdn %} + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script> + <script src="/assets/js/all.js"></script> + {% else %} + <script src="/assets/js/all-nocdn.js"></script> + {% endif %} + {% else %} + {% if use_cdn %} + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script> + {% else %} + <script src="/assets/js/jquery-1.11.0.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + {% endif %} + <script src="/assets/js/jquery.colorbox-min.js"></script> + {% endif %} +{% endblock %} + + +{% block html_navigation_links %} + {% for url, text in navigation_links[lang] %} + {% if url is mapping %} + <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }}<b class="caret"></b></a> + <ul class="dropdown-menu"> + {% for suburl, text in url %} + {% if rel_link(permalink, suburl) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a> + {% else %} + <li><a href="{{ suburl }}">{{ text }}</a> + {% endif %} + {% endfor %} + </ul> + {% else %} + {% if rel_link(permalink, url) == "#" %} + <li class="active"><a href="{{ permalink }}">{{ text }}</a> + {% else %} + <li><a href="{{ url }}">{{ text }}</a> + {% endif %} + {% endif %} + {% endfor %} +{% endblock %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl b/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl new file mode 100644 index 0000000..e3f9f05 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl @@ -0,0 +1,93 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} +{% import 'comments_helper.tmpl' as comments with context %} +{% import 'crumbs.tmpl' as ui with context %} +{% block sourcelink %}{% endblock %} + +{% block content %} + {{ ui.bar(crumbs) }} + {% if title %} + <h1>{{ title }}</h1> + {% endif %} + {% if post %} + <p> + {{ post.text() }} + </p> + {% endif %} + {% if folders %} + <ul> + {% for folder, ftitle in folders %} + <li><a href="{{ folder }}"><i class="icon-folder-open"></i> {{ ftitle }}</a></li> + {% endfor %} + </ul> + {% endif %} + +<div id="gallery_container"></div> +{% if photo_array %} +<noscript> +<ul class="thumbnails"> + {% for image in photo_array %} + <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title'] }}"> + <img src="{{ image['url_thumb'] }}" alt="{{ image['title'] }}" /></a> + {% endfor %} +</ul> +</noscript> +{% endif %} +{% if site_has_comments and enable_comments %} +{{ comments.comment_form(None, permalink, title) }} +{% endif %} +{% endblock %} + +{% block extra_head %} +{{ super() }} +<style type="text/css"> + .image-block { + display: inline-block; + } + .flowr_row { + width: 100%; + } + </style> +{% endblock %} + + +{% block extra_js %} +<script src="/assets/js/flowr.plugin.js"></script> +<script> +jsonContent = {{ photo_array_json }}; +$("#gallery_container").flowr({ + data : jsonContent, + height : {{ thumbnail_size }}*.6, + padding: 5, + rows: -1, + render : function(params) { + // Just return a div, string or a dom object, anything works fine + img = $("<img />").attr({ + 'src': params.itemData.url_thumb, + 'width' : params.width, + 'height' : params.height + }).css('max-width', '100%'); + link = $( "<a></a>").attr({ + 'href': params.itemData.url, + 'class': 'image-reference' + }); + div = $("<div />").addClass('image-block').attr({ + 'title': params.itemData.title, + 'data-toggle': "tooltip", + }); + link.append(img); + div.append(link); + div.hover(div.tooltip()); + return div; + }, + itemWidth : function(data) { return data.size.w; }, + itemHeight : function(data) { return data.size.h; }, + complete : function(params) { + if( jsonContent.length > params.renderedItems ) { + nextRenderList = jsonContent.slice( params.renderedItems ); + } + } + }); +$("a.image-reference").colorbox({rel:"gal", maxWidth:"100%",maxHeight:"100%",scalePhotos:true}); +</script> +{% endblock %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/listing.tmpl b/nikola/data/themes/bootstrap-jinja/templates/listing.tmpl new file mode 100644 index 0000000..4b99f86 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/listing.tmpl @@ -0,0 +1,28 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} +{% import 'crumbs.tmpl' as ui with context %} + +{% block content %} +{{ ui.bar(crumbs) }} +{% if folders or files %} +<ul class="list-unstyled"> +{% for name in folders %} + <li><a href="{{ name }}"><i class="icon-folder-open"></i> {{ name }}</a> +{% endfor %} +{% for name in files %} + <li><a href="{{ name }}.html"><i class="icon-file"></i> {{ name }}</a> +{% endfor %} +</ul> +{% endif %} +{% if code %} + {{ code }} +{% endif %} +{% endblock %} + +{% block sourcelink %} +{% if source_link %} + <li> + <a href="{{ source_link }}" id="sourcelink">{{ messages("Source") }}</a> + </li> +{% endif %} +{% endblock %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/post.tmpl b/nikola/data/themes/bootstrap-jinja/templates/post.tmpl new file mode 100644 index 0000000..531ebd5 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/post.tmpl @@ -0,0 +1,47 @@ +{# -*- coding: utf-8 -*- #} +{% import 'post_helper.tmpl' as helper with context %} +{% import 'post_header.tmpl' as pheader with context %} +{% import 'comments_helper.tmpl' as comments with context %} +{% extends 'base.tmpl' %} + +{% block extra_head %} + {{ super() }} + {% if post.meta('keywords') %} + <meta name="keywords" content="{{ post.meta('keywords')|e }}"> + {% endif %} + <meta name="author" content="{{ post.author() }}"> + {{ helper.open_graph_metadata(post) }} + {{ helper.twitter_card_information(post) }} + {{ helper.meta_translations(post) }} +{% endblock %} + +{% block content %} +<article class="post-{{ post.meta('type') }} h-entry hentry postpage" itemscope="itemscope" itemtype="http://schema.org/Article"> + {{ pheader.html_post_header() }} + <div class="e-content entry-content" itemprop="articleBody text"> + {{ post.text() }} + </div> + <aside class="postpromonav"> + <nav> + {{ helper.html_tags(post) }} + {{ helper.html_pager(post) }} + </nav> + </aside> + {% if not post.meta('nocomments') and site_has_comments %} + <section class="comments"> + <h2>{{ messages("Comments") }}</h2> + {{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }} + </section> + {% endif %} + {{ helper.mathjax_script(post) }} +</article> +{{ comments.comment_link_script() }} +{% endblock %} + +{% block sourcelink %} +{% if show_sourcelink %} + <li> + <a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a> + </li> +{% endif %} +{% endblock %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl b/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl new file mode 100644 index 0000000..b565244 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl @@ -0,0 +1,40 @@ +{# -*- coding: utf-8 -*- #} +{% import 'post_helper.tmpl' as helper with context %} +{% import 'comments_helper.tmpl' as comments with context %} + +{% macro html_title() %} + <h1 class="p-name entry-title" itemprop="headline name"><a href="{{ post.permalink() }}" class="u-url">{{ title|e }}</a></h1> +{% endmacro %} + +{% macro html_translations(post) %} + {% if translations|length > 1 %} + <div class="metadata posttranslations translations"> + <h3 class="posttranslations-intro">{{ messages("Also available in:") }}</h3> + {% for langname in translations.keys() %} + {% if langname != lang and post.is_translation_available(langname) %} + <p><a href="{{ post.permalink(langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></p> + {% endif %} + {% endfor %} + </div> + {% endif %} +{% endmacro %} + +{% macro html_post_header() %} + <header> + {{ html_title() }} + <div class="metadata"> + <p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p> + <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p> + {% if not post.meta('nocomments') and site_has_comments %} + <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} + {% endif %} + {% if post.meta('link') %} + <p><a href='{{ post.meta('link') }}'>{{ messages("Original site") }}</a></p> + {% endif %} + {% if post.description() %} + <meta content="{{ post.description() }}" itemprop="description"> + {% endif %} + </div> + {{ html_translations(post) }} + </header> +{% endmacro %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/slides.tmpl b/nikola/data/themes/bootstrap-jinja/templates/slides.tmpl new file mode 100644 index 0000000..0ae8fe8 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/slides.tmpl @@ -0,0 +1,24 @@ +{% block content %} +<div id="{{ carousel_id }}" class="carousel slide"> + <ol class="carousel-indicators"> + {% for i in range(slides_content|length) %} + {% if i == 0 %} + <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}" class="active"></li> + {% else %} + <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}"></li> + {% endif %} + {% endfor %} + </ol> + <div class="carousel-inner"> + {% for i, image in enumerate(slides_content) %} + {% if i == 0 %} + <div class="item active"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div> + {% else %} + <div class="item"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div> + {% endif %} + {% endfor %} + </div> + <a class="left carousel-control" href="#{{ carousel_id }}" data-slide="prev">‹</a> + <a class="right carousel-control" href="#{{ carousel_id }}" data-slide="next">›</a> +</div> +{% endblock %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl b/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl new file mode 100644 index 0000000..080e621 --- /dev/null +++ b/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl @@ -0,0 +1,26 @@ +{# -*- coding: utf-8 -*- #} +{% extends 'base.tmpl' %} + +{% block content %} +<h1>{{ title }}</h1> +{% if cat_items %} + <h2>{{ messages("Categories") }}</h2> + <ul class="unstyled"> + {% for text, link in cat_items %} + {% if text %} + <li><a class="reference badge" href="{{ link }}">{{ text }}</a></li> + {% endif %} + {% endfor %} + </ul> + {% if items %} + <h2>{{ messages("Tags") }}</h2> + {% endif %} +{% endif %} +{% if items %} + <ul class="list-inline"> + {% for text, link in items %} + <li><a class="reference badge" href="{{ link }}">{{ text }}</a></li> + {% endfor %} + </ul> +{% endif %} +{% endblock %} diff --git a/nikola/data/themes/bootstrap/assets/css/colorbox.css b/nikola/data/themes/bootstrap/assets/css/colorbox.css index 13c3308..5f8b3b0 100644..120000 --- a/nikola/data/themes/bootstrap/assets/css/colorbox.css +++ b/nikola/data/themes/bootstrap/assets/css/colorbox.css @@ -1,69 +1 @@ -/* - Colorbox Core Style: - The following CSS is consistent between example themes and should not be altered. -*/ -#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;} -#cboxOverlay{position:fixed; width:100%; height:100%;} -#cboxMiddleLeft, #cboxBottomLeft{clear:left;} -#cboxContent{position:relative;} -#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;} -#cboxTitle{margin:0;} -#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;} -#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;} -.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none; -ms-interpolation-mode:bicubic;} -.cboxIframe{width:100%; height:100%; display:block; border:0;} -#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;} - -/* - User Style: - Change the following styles to modify the appearance of Colorbox. They are - ordered & tabbed in a way that represents the nesting of the generated HTML. -*/ -#cboxOverlay{background:url(images/overlay.png) repeat 0 0;} -#colorbox{outline:0;} - #cboxTopLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px 0;} - #cboxTopRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px 0;} - #cboxBottomLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px -29px;} - #cboxBottomRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px -29px;} - #cboxMiddleLeft{width:21px; background:url(images/controls.png) left top repeat-y;} - #cboxMiddleRight{width:21px; background:url(images/controls.png) right top repeat-y;} - #cboxTopCenter{height:21px; background:url(images/border.png) 0 0 repeat-x;} - #cboxBottomCenter{height:21px; background:url(images/border.png) 0 -29px repeat-x;} - #cboxContent{background:#fff; overflow:hidden;} - .cboxIframe{background:#fff;} - #cboxError{padding:50px; border:1px solid #ccc;} - #cboxLoadedContent{margin-bottom:28px;} - #cboxTitle{position:absolute; bottom:4px; right: 29px; text-align: right; width:100%; color:#949494;} - #cboxCurrent{position:absolute; bottom:4px; left:58px; color:#949494;} - #cboxLoadingOverlay{background:url(images/loading_background.png) no-repeat center center;} - #cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;} - - /* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */ - #cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; width:auto; background:none; } - - /* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */ - #cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;} - - #cboxSlideshow{position:absolute; bottom:4px; right:30px; color:#0092ef;} - #cboxPrevious{position:absolute; bottom:0; left:0; background:url(images/controls.png) no-repeat -75px 0; width:25px; height:25px; text-indent:-9999px;} - #cboxPrevious:hover{background-position:-75px -25px;} - #cboxNext{position:absolute; bottom:0; left:27px; background:url(images/controls.png) no-repeat -50px 0; width:25px; height:25px; text-indent:-9999px;} - #cboxNext:hover{background-position:-50px -25px;} - #cboxClose{position:absolute; bottom:0; right:0; background:url(images/controls.png) no-repeat -25px 0; width:25px; height:25px; text-indent:-9999px;} - #cboxClose:hover{background-position:-25px -25px;} - -/* - The following fixes a problem where IE7 and IE8 replace a PNG's alpha transparency with a black fill - when an alpha filter (opacity change) is set on the element or ancestor element. This style is not applied to or needed in IE9. - See: http://jacklmoore.com/notes/ie-transparency-problems/ -*/ -.cboxIE #cboxTopLeft, -.cboxIE #cboxTopCenter, -.cboxIE #cboxTopRight, -.cboxIE #cboxBottomLeft, -.cboxIE #cboxBottomCenter, -.cboxIE #cboxBottomRight, -.cboxIE #cboxMiddleLeft, -.cboxIE #cboxMiddleRight { - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF); -} +../../../../../../bower_components/jquery-colorbox/example3/colorbox.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/css/images/border.png b/nikola/data/themes/bootstrap/assets/css/images/border.png Binary files differdeleted file mode 100644 index f463a10..0000000 --- a/nikola/data/themes/bootstrap/assets/css/images/border.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap/assets/css/images/controls.png b/nikola/data/themes/bootstrap/assets/css/images/controls.png Binary files differindex dcfd6fb..841a726 100644..120000 --- a/nikola/data/themes/bootstrap/assets/css/images/controls.png +++ b/nikola/data/themes/bootstrap/assets/css/images/controls.png diff --git a/nikola/data/themes/bootstrap/assets/css/images/loading.gif b/nikola/data/themes/bootstrap/assets/css/images/loading.gif Binary files differindex b4695d8..b192a75 100644..120000 --- a/nikola/data/themes/bootstrap/assets/css/images/loading.gif +++ b/nikola/data/themes/bootstrap/assets/css/images/loading.gif diff --git a/nikola/data/themes/bootstrap/assets/css/images/loading_background.png b/nikola/data/themes/bootstrap/assets/css/images/loading_background.png Binary files differdeleted file mode 100644 index 6ae83e6..0000000 --- a/nikola/data/themes/bootstrap/assets/css/images/loading_background.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap/assets/css/images/overlay.png b/nikola/data/themes/bootstrap/assets/css/images/overlay.png Binary files differdeleted file mode 100644 index 53ea98f..0000000 --- a/nikola/data/themes/bootstrap/assets/css/images/overlay.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap/assets/css/theme.css b/nikola/data/themes/bootstrap/assets/css/theme.css index 952073f..ccdfda2 100644 --- a/nikola/data/themes/bootstrap/assets/css/theme.css +++ b/nikola/data/themes/bootstrap/assets/css/theme.css @@ -101,3 +101,74 @@ h4, h5, h6 { margin-top: -50px; padding-top: 60px; } + +.image-block { + display: inline-block; +} + +.flowr_row { + width: 100%; +} + +.tags { + padding-left: 0; + margin-left: -5px; + list-style: none; + text-align: center; + +} + +.tags > li { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #999; + border-radius: 10px; +} + +.tags > li a { + color: #fff; +} + +.metadata p:before, +.postlist .listdate:before { + content: " — "; +} + +.metadata p:first-of-type:before { + content: ""; +} + +.metadata p { + display: inline; +} + +.posttranslations h3 { + display: inline; + font-size: 1em; + font-weight: bold; +} + +.posttranslations h3:last-child { + display: none; +} + +.entry-content { + margin-top: 1em; +} + +.navbar .brand { + padding: 0 20px; +} + +.navbar .brand #blog-title { + padding: 10px 0; + display: inline-block; +} diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js new file mode 120000 index 0000000..f83073f --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js new file mode 120000 index 0000000..bafc4e0 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js new file mode 120000 index 0000000..a749232 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js new file mode 120000 index 0000000..e4a595c --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js new file mode 120000 index 0000000..1e9a1d6 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js new file mode 120000 index 0000000..748f53b --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js new file mode 120000 index 0000000..1154fb5 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js new file mode 120000 index 0000000..483e192 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js new file mode 120000 index 0000000..a30b13c --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js new file mode 120000 index 0000000..2a7e8ad --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js new file mode 120000 index 0000000..e359290 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js new file mode 120000 index 0000000..04fa276 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js new file mode 120000 index 0000000..d8105ab --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js new file mode 120000 index 0000000..72dddf5 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js new file mode 120000 index 0000000..34aa3c0 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js new file mode 120000 index 0000000..a87f03c --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js new file mode 120000 index 0000000..31053b8 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js new file mode 120000 index 0000000..aad9d22 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js new file mode 120000 index 0000000..3ea27c2 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js new file mode 120000 index 0000000..3e23b4a --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js new file mode 120000 index 0000000..374b9bb --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js new file mode 120000 index 0000000..101b476 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js new file mode 120000 index 0000000..8e14f15 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js new file mode 120000 index 0000000..2d03d48 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js new file mode 120000 index 0000000..9af0ba7 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js new file mode 120000 index 0000000..34f8ab1 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js new file mode 120000 index 0000000..76f289e --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-br.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js new file mode 120000 index 0000000..555f2e6 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js new file mode 120000 index 0000000..bac4855 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js new file mode 120000 index 0000000..65b0492 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js new file mode 120000 index 0000000..99859fd --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js new file mode 120000 index 0000000..c4fd9d5 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js new file mode 120000 index 0000000..d7f26e0 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js new file mode 120000 index 0000000..86fd98f --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js new file mode 120000 index 0000000..7cd1336 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js new file mode 120000 index 0000000..e6c5965 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js new file mode 120000 index 0000000..bd2254c --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js @@ -0,0 +1 @@ +../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js new file mode 120000 index 0000000..5ee7a90 --- /dev/null +++ b/nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js @@ -0,0 +1 @@ +../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap/bundles b/nikola/data/themes/bootstrap/bundles index 14124a3..089b036 100644 --- a/nikola/data/themes/bootstrap/bundles +++ b/nikola/data/themes/bootstrap/bundles @@ -1,4 +1,4 @@ assets/css/all-nocdn.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,theme.css,custom.css assets/css/all.css=rst.css,code.css,colorbox.css,theme.css,custom.css -assets/js/all-nocdn.js=jquery-1.10.2.min.js,bootstrap.min.js,jquery.colorbox-min.js +assets/js/all-nocdn.js=jquery.min.js,bootstrap.min.js,jquery.colorbox-min.js assets/js/all.js=jquery.colorbox-min.js diff --git a/nikola/data/themes/bootstrap/templates/base.tmpl b/nikola/data/themes/bootstrap/templates/base.tmpl index 65132b7..a469098 100644 --- a/nikola/data/themes/bootstrap/templates/base.tmpl +++ b/nikola/data/themes/bootstrap/templates/base.tmpl @@ -1,28 +1,17 @@ ## -*- coding: utf-8 -*- <%namespace name="base" file="base_helper.tmpl" import="*" /> -<%namespace name="bootstrap" file="bootstrap_helper.tmpl" import="*" /> <%namespace name="notes" file="annotation_helper.tmpl" import="*" /> ${set_locale(lang)} -<!DOCTYPE html> -<html -%if comment_system == 'facebook': -xmlns:fb="http://ogp.me/ns/fb#" -%endif -lang="${lang}"> -<head> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - ${bootstrap.html_head()} - <%block name="extra_head"> - </%block> - % if annotations and post and not post.meta('noannotations'): - ${notes.css()} - % elif not annotations and post and post.meta('annotations'): - ${notes.css()} - % endif - ${extra_head_data} +${base.html_headstart()} +<%block name="extra_head"> +### Leave this block alone. +</%block> +${template_hooks['extra_head']()} </head> <body> + <!-- Menubar --> + <div class="navbar navbar-fixed-top" id="navbar"> <div class="navbar-inner"> <div class="container"> @@ -35,12 +24,19 @@ lang="${lang}"> </a> <a class="brand" href="${abs_link('/')}"> - ${blog_title} + %if logo_url: + <img src="${logo_url}" alt="${blog_title}" id="logo"> + %endif + + % if show_blog_title: + <span id="blog-title">${blog_title}</span> + % endif </a> <!-- Everything you want hidden at 940px or less, place within here --> <div class="nav-collapse collapse"> <ul class="nav"> - ${bootstrap.html_navigation_links()} + ${base.html_navigation_links()} + ${template_hooks['menu']()} </ul> %if search_form: ${search_form} @@ -51,9 +47,10 @@ lang="${lang}"> <li>${base.html_translations()}</li> %endif </%block> - % if not hide_sourcelink: + % if show_sourcelink: <li><%block name="sourcelink"></%block></li> %endif + ${template_hooks['menu_alt']()} </ul> </div> </div> @@ -65,6 +62,7 @@ lang="${lang}"> <div class="row-fluid"> <div class="span2"></div> <div class="span8"> + ${template_hooks['page_header']()} <%block name="content"></%block> </div> </div> @@ -72,23 +70,17 @@ lang="${lang}"> </div> <div class="footerbox"> ${content_footer} + ${template_hooks['page_footer']()} </div> -${bootstrap.late_load_js()} -${base.html_social()} - <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true}); - $(window).on('hashchange', function(){ - if (location.hash && $(location.hash)[0]) { - $('body').animate({scrollTop: $(location.hash).offset().top - $('#navbar').outerHeight(true)*1.2 }, 1); - } - }); - $(document).ready(function(){$(window).trigger('hashchange')}); - </script> +${base.late_load_js()} + <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script> <%block name="extra_js"></%block> % if annotations and post and not post.meta('noannotations'): ${notes.code()} % elif not annotations and post and post.meta('annotations'): ${notes.code()} % endif - ${body_end} +${body_end} +${template_hooks['body_end']()} </body> </html> diff --git a/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl b/nikola/data/themes/bootstrap/templates/base_helper.tmpl index c041e50..2dcc138 100644 --- a/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl +++ b/nikola/data/themes/bootstrap/templates/base_helper.tmpl @@ -1,77 +1,117 @@ -## Override only the functions that differ from base_helper.tmpl -<%def name="html_head()"> +## -*- coding: utf-8 -*- + +<%def name="html_headstart()"> +<!DOCTYPE html> +<html +\ +% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook'): +prefix='\ +%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']): +og: http://ogp.me/ns# \ +%endif +%if use_open_graph: +article: http://ogp.me/ns/article# \ +%endif +%if comment_system == 'facebook': +fb: http://ogp.me/ns/fb# \ +%endif +'\ +%endif +\ +% if is_rtl: +dir="rtl" \ +% endif +\ +lang="${lang}"> + <head> <meta charset="utf-8"> %if description: <meta name="description" content="${description}"> %endif - <meta name="author" content="${blog_author}"> + <meta name="viewport" content="width=device-width"> <title>${title|striphtml} | ${blog_title|striphtml}</title> + + ${html_stylesheets()} + ${html_feedlinks()} + %if permalink: + <link rel="canonical" href="${abs_link(permalink)}"> + %endif + + %if favicons: + %for name, file, size in favicons: + <link rel="${name}" href="${file}" sizes="${size}"/> + %endfor + %endif + + % if comment_system == 'facebook': + <meta property="fb:app_id" content="${comment_system_id}"> + % endif + ${mathjax_config} + %if use_cdn: + <!--[if lt IE 9]><script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + %else: + <!--[if lt IE 9]><script src="/assets/js/html5.js"></script><![endif]--> + %endif + + ${extra_head_data} +</%def> + + +<%def name="late_load_js()"> %if use_bundles: %if use_cdn: - <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet"> - <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script> + <script src="/assets/js/all.js"></script> %else: - <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + <script src="/assets/js/all-nocdn.js"></script> %endif %else: %if use_cdn: - <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet"> - %else: - <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css"> - %endif - <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> - <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> - %if has_custom_css: - <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> - %endif - %endif - %if permalink: - <link rel="canonical" href="${abs_link(permalink)}"> - %endif - <!--[if lt IE 9]> - <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> - <![endif]--> - %if rss_link: - ${rss_link} - %else: - %if len(translations) > 1: - %for language in translations: - <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}"> - %endfor + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> + <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script> %else: - <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}"> + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> %endif + <script src="/assets/js/jquery.colorbox-min.js"></script> %endif - %if favicons: - %for name, file, size in favicons: - <link rel="${name}" href="${file}" sizes="${size}"/> - %endfor + %if colorbox_locales[lang]: + <script src="/assets/js/colorbox-i18n/jquery.colorbox-${colorbox_locales[lang]}.js"></script> %endif + ${social_buttons_code} </%def> -<%def name="late_load_js()"> + +<%def name="html_stylesheets()"> %if use_bundles: %if use_cdn: - <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" type="text/javascript"></script> - <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script> - <script src="/assets/js/all.js" type="text/javascript"></script> + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet"> + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> %else: - <script src="/assets/js/all-nocdn.js" type="text/javascript"></script> + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> %endif %else: %if use_cdn: - <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" type="text/javascript"></script> - <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script> + <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet"> %else: - <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script> - <script src="/assets/js/bootstrap.min.js" type="text/javascript"></script> + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css"> + %endif + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"> + %if has_custom_css: + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> %endif - <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> %endif + % if annotations and post and not post.meta('noannotations'): + ${notes.css()} + % elif not annotations and post and post.meta('annotations'): + ${notes.css()} + % endif </%def> @@ -97,3 +137,25 @@ % endif %endfor </%def> + +<%def name="html_feedlinks()"> + %if rss_link: + ${rss_link} + %elif generate_rss: + %if len(translations) > 1: + %for language in translations: + <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}"> + %endfor + %else: + <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}"> + %endif + %endif +</%def> + +<%def name="html_translations()"> + %for langname in translations.keys(): + %if langname != lang: + <li><a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li> + %endif + %endfor +</%def> diff --git a/nikola/data/themes/bootstrap/templates/gallery.tmpl b/nikola/data/themes/bootstrap/templates/gallery.tmpl index 7b0d505..8ad4eb4 100644 --- a/nikola/data/themes/bootstrap/templates/gallery.tmpl +++ b/nikola/data/themes/bootstrap/templates/gallery.tmpl @@ -9,38 +9,37 @@ %if title: <h1>${title}</h1> %endif - %if text: + %if post: <p> - ${text} + ${post.text()} </p> %endif %if folders: <ul> % for folder, ftitle in folders: - <li><a href="${folder}"><i - class="icon-folder-open"></i> ${ftitle}</a></li> + <li><a href="${folder}"><i class="icon-folder-open"></i> ${ftitle}</a></li> % endfor </ul> %endif - <div id="gallery_container"></div> - %if photo_array: - <noscript> - <ul class="thumbnails"> - %for image in photo_array: - <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}"> - <img src="${image['url_thumb']}" alt="${image['title']}" /></a> - %endfor - </ul> - </noscript> - %endif -%if enable_comments: - ${comments.comment_form(None, permalink, title)} +<div id="gallery_container"></div> +%if photo_array: +<noscript> +<ul class="thumbnails"> + %for image in photo_array: + <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}"> + <img src="${image['url_thumb']}" alt="${image['title']}" /></a> + %endfor +</ul> +</noscript> +%endif +%if site_has_comments and enable_comments: +${comments.comment_form(None, permalink, title)} %endif </%block> - <%block name="extra_head"> +${parent.extra_head()} <style type="text/css"> .image-block { display: inline-block; diff --git a/nikola/data/themes/bootstrap/templates/listing.tmpl b/nikola/data/themes/bootstrap/templates/listing.tmpl new file mode 100644 index 0000000..f03ea23 --- /dev/null +++ b/nikola/data/themes/bootstrap/templates/listing.tmpl @@ -0,0 +1,28 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%namespace name="ui" file="crumbs.tmpl" import="bar"/> + +<%block name="content"> +${ui.bar(crumbs)} +%if folders or files: +<ul class="list-unstyled"> +% for name in folders: + <li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a> +% endfor +% for name in files: + <li><a href="${name}.html"><i class="icon-file"></i> ${name}</a> +% endfor +</ul> +%endif +% if code: + ${code} +% endif +</%block> + +<%block name="sourcelink"> +% if source_link: + <li> + <a href="${source_link}" id="sourcelink">${messages("Source")}</a> + </li> +% endif +</%block> diff --git a/nikola/data/themes/bootstrap/templates/post.tmpl b/nikola/data/themes/bootstrap/templates/post.tmpl new file mode 100644 index 0000000..29a5b75 --- /dev/null +++ b/nikola/data/themes/bootstrap/templates/post.tmpl @@ -0,0 +1,47 @@ +## -*- coding: utf-8 -*- +<%namespace name="helper" file="post_helper.tmpl"/> +<%namespace name="pheader" file="post_header.tmpl"/> +<%namespace name="comments" file="comments_helper.tmpl"/> +<%inherit file="base.tmpl"/> + +<%block name="extra_head"> + ${parent.extra_head()} + % if post.meta('keywords'): + <meta name="keywords" content="${post.meta('keywords')|h}"> + % endif + <meta name="author" content="${post.author()}"> + ${helper.open_graph_metadata(post)} + ${helper.twitter_card_information(post)} + ${helper.meta_translations(post)} +</%block> + +<%block name="content"> +<article class="post-${post.meta('type')} h-entry hentry postpage" itemscope="itemscope" itemtype="http://schema.org/Article"> + ${pheader.html_post_header()} + <div class="e-content entry-content" itemprop="articleBody text"> + ${post.text()} + </div> + <aside class="postpromonav"> + <nav> + ${helper.html_tags(post)} + ${helper.html_pager(post)} + </nav> + </aside> + % if not post.meta('nocomments') and site_has_comments: + <section class="comments"> + <h2>${messages("Comments")}</h2> + ${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)} + </section> + % endif + ${helper.mathjax_script(post)} +</article> +${comments.comment_link_script()} +</%block> + +<%block name="sourcelink"> +% if show_sourcelink: + <li> + <a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a> + </li> +% endif +</%block> diff --git a/nikola/data/themes/bootstrap/templates/slides.tmpl b/nikola/data/themes/bootstrap/templates/slides.tmpl index 14983ad..048fb7e 100644 --- a/nikola/data/themes/bootstrap/templates/slides.tmpl +++ b/nikola/data/themes/bootstrap/templates/slides.tmpl @@ -1,6 +1,7 @@ +<%block name="content"> <div id="${carousel_id}" class="carousel slide"> <ol class="carousel-indicators"> - % for i in range(len(content)): + % for i in range(len(slides_content)): % if i == 0: <li data-target="#${carousel_id}" data-slide-to="${i}" class="active"></li> % else: @@ -9,7 +10,7 @@ % endfor </ol> <div class="carousel-inner"> - % for i, image in enumerate(content): + % for i, image in enumerate(slides_content): % if i == 0: <div class="item active"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div> % else: @@ -20,3 +21,4 @@ <a class="left carousel-control" href="#${carousel_id}" data-slide="prev">‹</a> <a class="right carousel-control" href="#${carousel_id}" data-slide="next">›</a> </div> +</%block> diff --git a/nikola/data/themes/bootstrap/templates/tags.tmpl b/nikola/data/themes/bootstrap/templates/tags.tmpl new file mode 100644 index 0000000..9afeca7 --- /dev/null +++ b/nikola/data/themes/bootstrap/templates/tags.tmpl @@ -0,0 +1,26 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> + +<%block name="content"> +<h1>${title}</h1> +% if cat_items: + <h2>${messages("Categories")}</h2> + <ul class="unstyled"> + % for text, link in cat_items: + % if text: + <li><a class="reference badge" href="${link}">${text}</a></li> + % endif + % endfor + </ul> + % if items: + <h2>${messages("Tags")}</h2> + % endif +%endif +% if items: + <ul class="list-inline"> + % for text, link in items: + <li><a class="reference badge" href="${link}">${text}</a></li> + % endfor + </ul> +% endif +</%block> diff --git a/nikola/filters.py b/nikola/filters.py index aa7ba8a..78d624b 100644 --- a/nikola/filters.py +++ b/nikola/filters.py @@ -29,7 +29,7 @@ from .utils import req_missing from functools import wraps import os -import re +import codecs import shutil import subprocess import tempfile @@ -41,10 +41,10 @@ except ImportError: typo = None # NOQA -def apply_to_file(f): - """Takes a function f that transforms a data argument, and returns +def apply_to_binary_file(f): + """Take a function f that transforms a data argument, and returns a function that takes a filename and applies f to the contents, - in place.""" + in place. Reads files in binary mode.""" @wraps(f) def f_in_file(fname): with open(fname, 'rb') as inf: @@ -56,15 +56,30 @@ def apply_to_file(f): return f_in_file +def apply_to_text_file(f): + """Take a function f that transforms a data argument, and returns + a function that takes a filename and applies f to the contents, + in place. Reads files in UTF-8.""" + @wraps(f) + def f_in_file(fname): + with codecs.open(fname, 'r', 'utf-8') as inf: + data = inf.read() + data = f(data) + with codecs.open(fname, 'w+', 'utf-8') as outf: + outf.write(data) + + return f_in_file + + def list_replace(the_list, find, replacement): - "Replaces all occurrences of ``find`` with ``replacement`` in ``the_list``" + "Replace all occurrences of ``find`` with ``replacement`` in ``the_list``" for i, v in enumerate(the_list): if v == find: the_list[i] = replacement def runinplace(command, infile): - """Runs a command in-place on a file. + """Run a command in-place on a file. command is a string of the form: "commandname %1 %2" and it will be execed with infile as %1 and a temporary file @@ -128,60 +143,15 @@ def jpegoptim(infile): return runinplace(r"jpegoptim -p --strip-all -q %1", infile) -def tidy(inplace): - # Google site verifcation files are not HTML - if re.match(r"google[a-f0-9]+.html", os.path.basename(inplace)) \ - and open(inplace).readline().startswith( - "google-site-verification:"): - return - - # Tidy will give error exits, that we will ignore. - output = subprocess.check_output( - "tidy -m -w 90 --indent no --quote-marks" - "no --keep-time yes --tidy-mark no " - "--force-output yes '{0}'; exit 0".format(inplace), stderr=subprocess.STDOUT, shell=True) - - for line in output.split("\n"): - if "Warning:" in line: - if '<meta> proprietary attribute "charset"' in line: - # We want to set it though. - continue - elif '<meta> lacks "content" attribute' in line: - # False alarm to me. - continue - elif '<div> anchor' in line and 'already defined' in line: - # Some seeming problem with JavaScript terminators. - continue - elif '<img> lacks "alt" attribute' in line: - # Happens in gallery code, probably can be tolerated. - continue - elif '<table> lacks "summary" attribute' in line: - # Happens for tables, TODO: Check this is normal. - continue - elif 'proprietary attribute "data-toggle"' in line or \ - 'proprietary attribute "data-target"': - # Some of our own tricks - continue - else: - assert False, (inplace, line) - elif "Error:" in line: - if '<time> is not recognized' in line: - # False alarm, time is proper HTML5. - continue - else: - assert False, line - - -@apply_to_file +@apply_to_text_file def typogrify(data): - global typogrify_filter if typo is None: - req_missing(['typogrify', 'use the typogrify filter']) + req_missing(['typogrify'], 'use the typogrify filter') data = typo.amp(data) data = typo.widont(data) data = typo.smartypants(data) # Disabled because of typogrify bug where it breaks <title> - #data = typo.caps(data) + # data = typo.caps(data) data = typo.initial_quotes(data) return data diff --git a/nikola/nikola.py b/nikola/nikola.py index 1d59954..59e1b97 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -28,6 +28,7 @@ from __future__ import print_function, unicode_literals import codecs from collections import defaultdict from copy import copy +from pkg_resources import resource_filename import datetime import glob import locale @@ -43,7 +44,7 @@ try: import pyphen except ImportError: pyphen = None -import pytz +import dateutil.tz import logging from . import DEBUG @@ -53,9 +54,18 @@ if DEBUG: else: logging.basicConfig(level=logging.ERROR) +import PyRSS2Gen as rss + import lxml.html from yapsy.PluginManager import PluginManager +# Default "Read more..." link +DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>' +DEFAULT_RSS_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>' + +# Default pattern for translation files' names +DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}' + from .post import Post from . import utils from .plugin_categories import ( @@ -63,20 +73,94 @@ from .plugin_categories import ( LateTask, PageCompiler, RestExtension, + MarkdownExtension, Task, TaskMultiplier, TemplateSystem, SignalHandler, ) -from .utils import ColorfulStderrHandler config_changed = utils.config_changed __all__ = ['Nikola'] -# Default pattern for translation files' names -DEFAULT_TRANSLATIONS_PATTERN = '{path}.{ext}.{lang}' +# We store legal values for some setting here. For internal use. +LEGAL_VALUES = { + 'COMMENT_SYSTEM': [ + 'disqus', + 'facebook', + 'googleplus', + 'intensedebate', + 'isso', + 'livefyre', + 'muut', + ], + 'TRANSLATIONS': { + 'bg': 'Bulgarian', + 'ca': 'Catalan', + ('cs', 'cz'): 'Czech', + 'de': 'German', + ('el', '!gr'): 'Greek', + 'en': 'English', + 'eo': 'Esperanto', + 'es': 'Spanish', + 'et': 'Estonian', + 'eu': 'Basque', + 'fa': 'Persian', + 'fi': 'Finnish', + 'fr': 'French', + 'hi': 'Hindi', + 'hr': 'Croatian', + 'it': 'Italian', + ('ja', '!jp'): 'Japanese', + 'nb': 'Norwegian Bokmål', + 'nl': 'Dutch', + 'pl': 'Polish', + 'pt_br': 'Portuguese (Brasil)', + 'ru': 'Russian', + 'sk': 'Slovak', + 'sl': 'Slovene', + ('tr', '!tr_TR'): 'Turkish', + 'ur': 'Urdu', + 'zh_cn': 'Chinese (Simplified)', + }, + '_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS': { + # This dict is used in `init` in case of locales that exist with a + # country specifier. If there is no other locale that has the same + # language with a different country, ``nikola init`` (but nobody else!) + # will accept it, warning the user about it. + 'pt': 'pt_br', + 'zh': 'zh_cn' + }, + 'RTL_LANGUAGES': ('fa', 'ur'), + 'COLORBOX_LOCALES': defaultdict( + str, + bg='bg', + ca='ca', + cs='cs', + cz='cs', + de='de', + en='', + es='es', + et='et', + fa='fa', + fi='fi', + fr='fr', + hr='hr', + it='it', + ja='ja', + nb='no', + nl='nl', + pt_br='pt-br', + pl='pl', + ru='ru', + sk='sk', + sl='si', # country code is si, language code is sl, colorbox is wrong + tr='tr', + zh_cn='zh-CN' + ) +} class Nikola(object): @@ -85,12 +169,6 @@ class Nikola(object): Takes a site config as argument on creation. """ - EXTRA_PLUGINS = [ - 'planetoid', - 'ipynb', - 'local_search', - 'render_mustache', - ] def __init__(self, **config): """Setup proper environment for running tasks.""" @@ -117,24 +195,29 @@ class Nikola(object): self._THEMES = None self.debug = DEBUG self.loghandlers = [] - if not config: - self.configured = False - self.colorful = False - else: - self.configured = True - self.colorful = config.pop('__colorful__', False) - - ColorfulStderrHandler._colorful = self.colorful + self.colorful = config.pop('__colorful__', False) + self.invariant = config.pop('__invariant__', False) + self.quiet = config.pop('__quiet__', False) + self.configured = bool(config) + + self.template_hooks = { + 'extra_head': utils.TemplateHookRegistry('extra_head', self), + 'body_end': utils.TemplateHookRegistry('body_end', self), + 'page_header': utils.TemplateHookRegistry('page_header', self), + 'menu': utils.TemplateHookRegistry('menu', self), + 'menu_alt': utils.TemplateHookRegistry('menu_alt', self), + 'page_footer': utils.TemplateHookRegistry('page_footer', self), + } # Maintain API utils.generic_rss_renderer = self.generic_rss_renderer # This is the default config self.config = { - 'ADD_THIS_BUTTONS': True, 'ANNOTATIONS': False, 'ARCHIVE_PATH': "", 'ARCHIVE_FILENAME': "archive.html", + 'BLOG_AUTHOR': 'Default Author', 'BLOG_TITLE': 'Default Title', 'BLOG_DESCRIPTION': 'Default Description', 'BODY_END': "", @@ -154,16 +237,16 @@ class Nikola(object): "html": ('.html', '.htm') }, 'CONTENT_FOOTER': '', + 'CONTENT_FOOTER_FORMATS': {}, 'COPY_SOURCES': True, 'CREATE_MONTHLY_ARCHIVE': False, 'CREATE_SINGLE_ARCHIVE': False, 'DATE_FORMAT': '%Y-%m-%d %H:%M', 'DEFAULT_LANG': "en", 'DEPLOY_COMMANDS': [], - 'DISABLED_PLUGINS': (), + 'DISABLED_PLUGINS': [], 'EXTRA_PLUGINS_DIRS': [], 'COMMENT_SYSTEM_ID': 'nikolademo', - 'ENABLED_EXTRAS': (), 'EXTRA_HEAD_DATA': '', 'FAVICONS': {}, 'FEED_LENGTH': 10, @@ -171,13 +254,12 @@ class Nikola(object): 'ADDITIONAL_METADATA': {}, 'FILES_FOLDERS': {'files': ''}, 'FILTERS': {}, + 'FORCE_ISO8601': False, 'GALLERY_PATH': 'galleries', 'GALLERY_SORT_BY_DATE': True, 'GZIP_COMMAND': None, 'GZIP_FILES': False, 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'), - 'HIDE_SOURCELINK': False, - 'HIDE_UNTRANSLATED_POSTS': False, 'HYPHENATE': False, 'INDEX_DISPLAY_POST_COUNT': 10, 'INDEX_FILE': 'index.html', @@ -192,7 +274,8 @@ class Nikola(object): 'LICENSE': '', 'LINK_CHECK_WHITELIST': [], 'LISTINGS_FOLDER': 'listings', - 'NAVIGATION_LINKS': None, + 'LOGO_URL': '', + 'NAVIGATION_LINKS': {}, 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'], 'MAX_IMAGE_SIZE': 1280, 'MATHJAX_CONFIG': '', @@ -202,14 +285,21 @@ class Nikola(object): 'PAGES': (("stories/*.txt", "stories", "story.tmpl"),), 'PRETTY_URLS': False, 'FUTURE_IS_NOW': False, - 'READ_MORE_LINK': '<p class="more"><a href="{link}">{read_more}…</a></p>', + 'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK, + 'RSS_READ_MORE_LINK': DEFAULT_RSS_READ_MORE_LINK, 'REDIRECTIONS': [], + 'ROBOTS_EXCLUSIONS': [], + 'GENERATE_RSS': True, 'RSS_LINK': None, 'RSS_PATH': '', + 'RSS_PLAIN': False, 'RSS_TEASERS': True, 'SASS_COMPILER': 'sass', 'SASS_OPTIONS': [], 'SEARCH_FORM': '', + 'SHOW_BLOG_TITLE': True, + 'SHOW_SOURCELINK': True, + 'SHOW_UNTRANSLATED_POSTS': True, 'SLUG_TAG_PATH': True, 'SOCIAL_BUTTONS_CODE': SOCIAL_BUTTONS_CODE, 'SITE_URL': 'http://getnikola.com/', @@ -223,23 +313,82 @@ class Nikola(object): 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky', 'THEME_REVEAL_CONFIG_TRANSITION': 'cube', 'THUMBNAIL_SIZE': 180, + 'UNSLUGIFY_TITLES': False, # WARNING: conf.py.in overrides this with True for backwards compatibility 'URL_TYPE': 'rel_path', 'USE_BUNDLES': True, 'USE_CDN': False, 'USE_FILENAME_AS_TITLE': True, + 'USE_OPEN_GRAPH': True, 'TIMEZONE': 'UTC', 'DEPLOY_DRAFTS': True, 'DEPLOY_FUTURE': False, 'SCHEDULE_ALL': False, 'SCHEDULE_RULE': '', - 'SCHEDULE_FORCE_TODAY': False, 'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}}, 'DEMOTE_HEADERS': 1, - 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, } + # set global_context for template rendering + self._GLOBAL_CONTEXT = {} + self.config.update(config) + # __builtins__ contains useless cruft + if '__builtins__' in self.config: + try: + del self.config['__builtins__'] + except KeyError: + del self.config[b'__builtins__'] + + self.config['__colorful__'] = self.colorful + self.config['__invariant__'] = self.invariant + self.config['__quiet__'] = self.quiet + + # Make sure we have sane NAVIGATION_LINKS. + if not self.config['NAVIGATION_LINKS']: + self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()} + + # Translatability configuration. + self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS', + {self.config['DEFAULT_LANG']: ''}) + utils.TranslatableSetting.default_lang = self.config['DEFAULT_LANG'] + + self.TRANSLATABLE_SETTINGS = ('BLOG_AUTHOR', + 'BLOG_TITLE', + 'BLOG_DESCRIPTION', + 'LICENSE', + 'CONTENT_FOOTER', + 'SOCIAL_BUTTONS_CODE', + 'SEARCH_FORM', + 'BODY_END', + 'EXTRA_HEAD_DATA', + 'NAVIGATION_LINKS', + 'INDEX_READ_MORE_LINK', + 'RSS_READ_MORE_LINK',) + + self._GLOBAL_CONTEXT_TRANSLATABLE = ('blog_author', + 'blog_title', + 'blog_desc', # TODO: remove in v8 + 'blog_description', + 'license', + 'content_footer', + 'social_buttons_code', + 'search_form', + 'body_end', + 'extra_head_data',) + # WARNING: navigation_links SHOULD NOT be added to the list above. + # Themes ask for [lang] there and we should provide it. + + for i in self.TRANSLATABLE_SETTINGS: + try: + self.config[i] = utils.TranslatableSetting(i, self.config[i], self.config['TRANSLATIONS']) + except KeyError: + pass + + # Handle CONTENT_FOOTER properly. + # We provide the arguments to format in CONTENT_FOOTER_FORMATS. + self.config['CONTENT_FOOTER'].langformat(self.config['CONTENT_FOOTER_FORMATS']) + # Make sure we have pyphen installed if we are using it if self.config.get('HYPHENATE') and pyphen is None: utils.LOGGER.warn('To use the hyphenation, you have to install ' @@ -247,24 +396,6 @@ class Nikola(object): utils.LOGGER.warn('Setting HYPHENATE to False.') self.config['HYPHENATE'] = False - # Deprecating post_compilers - # TODO: remove on v7 - if 'post_compilers' in config: - utils.LOGGER.warn('The post_compilers option is deprecated, use COMPILERS instead.') - if 'COMPILERS' in config: - utils.LOGGER.warn('COMPILERS conflicts with post_compilers, ignoring post_compilers.') - else: - self.config['COMPILERS'] = config['post_compilers'] - - # Deprecating post_pages - # TODO: remove on v7 - if 'post_pages' in config: - utils.LOGGER.warn('The post_pages option is deprecated, use POSTS and PAGES instead.') - if 'POSTS' in config or 'PAGES' in config: - utils.LOGGER.warn('POSTS and PAGES conflict with post_pages, ignoring post_pages.') - else: - self.config['POSTS'] = [item[:3] for item in config['post_pages'] if item[-1]] - self.config['PAGES'] = [item[:3] for item in config['post_pages'] if not item[-1]] # FIXME: Internally, we still use post_pages because it's a pain to change it self.config['post_pages'] = [] for i1, i2, i3 in self.config['POSTS']: @@ -272,80 +403,78 @@ class Nikola(object): for i1, i2, i3 in self.config['PAGES']: self.config['post_pages'].append([i1, i2, i3, False]) - # Deprecating DISQUS_FORUM - # TODO: remove on v7 - if 'DISQUS_FORUM' in config: - utils.LOGGER.warn('The DISQUS_FORUM option is deprecated, use COMMENT_SYSTEM_ID instead.') - if 'COMMENT_SYSTEM_ID' in config: - utils.LOGGER.warn('DISQUS_FORUM conflicts with COMMENT_SYSTEM_ID, ignoring DISQUS_FORUM.') + # DEFAULT_TRANSLATIONS_PATTERN was changed from "p.e.l" to "p.l.e" + # TODO: remove on v8 + if 'TRANSLATIONS_PATTERN' not in self.config: + if len(self.config.get('TRANSLATIONS', {})) > 1: + utils.LOGGER.warn('You do not have a TRANSLATIONS_PATTERN set in your config, yet you have multiple languages.') + utils.LOGGER.warn('Setting TRANSLATIONS_PATTERN to the pre-v6 default ("{path}.{ext}.{lang}").') + utils.LOGGER.warn('Please add the proper pattern to your conf.py. (The new default in v7 is "{0}".)'.format(DEFAULT_TRANSLATIONS_PATTERN)) + self.config['TRANSLATIONS_PATTERN'] = "{path}.{ext}.{lang}" else: - self.config['COMMENT_SYSTEM_ID'] = config['DISQUS_FORUM'] - - # Deprecating the ANALYTICS option - # TODO: remove on v7 - if 'ANALYTICS' in config: - utils.LOGGER.warn('The ANALYTICS option is deprecated, use BODY_END instead.') - if 'BODY_END' in config: - utils.LOGGER.warn('ANALYTICS conflicts with BODY_END, ignoring ANALYTICS.') + # use v7 default there + self.config['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN + + # HIDE_SOURCELINK has been replaced with the inverted SHOW_SOURCELINK + # TODO: remove on v8 + if 'HIDE_SOURCELINK' in config: + utils.LOGGER.warn('The HIDE_SOURCELINK option is deprecated, use SHOW_SOURCELINK instead.') + if 'SHOW_SOURCELINK' in config: + utils.LOGGER.warn('HIDE_SOURCELINK conflicts with SHOW_SOURCELINK, ignoring HIDE_SOURCELINK.') + self.config['SHOW_SOURCELINK'] = not config['HIDE_SOURCELINK'] + + # HIDE_UNTRANSLATED_POSTS has been replaced with the inverted SHOW_UNTRANSLATED_POSTS + # TODO: remove on v8 + if 'HIDE_UNTRANSLATED_POSTS' in config: + utils.LOGGER.warn('The HIDE_UNTRANSLATED_POSTS option is deprecated, use SHOW_UNTRANSLATED_POSTS instead.') + if 'SHOW_UNTRANSLATED_POSTS' in config: + utils.LOGGER.warn('HIDE_UNTRANSLATED_POSTS conflicts with SHOW_UNTRANSLATED_POSTS, ignoring HIDE_UNTRANSLATED_POSTS.') + self.config['SHOW_UNTRANSLATED_POSTS'] = not config['HIDE_UNTRANSLATED_POSTS'] + + # READ_MORE_LINK has been split into INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK + # TODO: remove on v8 + if 'READ_MORE_LINK' in config: + utils.LOGGER.warn('The READ_MORE_LINK option is deprecated, use INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK instead.') + if 'INDEX_READ_MORE_LINK' in config: + utils.LOGGER.warn('READ_MORE_LINK conflicts with INDEX_READ_MORE_LINK, ignoring READ_MORE_LINK.') else: - self.config['BODY_END'] = config['ANALYTICS'] - - # Deprecating the SIDEBAR_LINKS option - # TODO: remove on v7 - if 'SIDEBAR_LINKS' in config: - utils.LOGGER.warn('The SIDEBAR_LINKS option is deprecated, use NAVIGATION_LINKS instead.') - if 'NAVIGATION_LINKS' in config: - utils.LOGGER.warn('The SIDEBAR_LINKS conflicts with NAVIGATION_LINKS, ignoring SIDEBAR_LINKS.') + self.config['INDEX_READ_MORE_LINK'] = utils.TranslatableSetting('INDEX_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS']) + + if 'RSS_READ_MORE_LINK' in config: + utils.LOGGER.warn('READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring READ_MORE_LINK.') else: - self.config['NAVIGATION_LINKS'] = config['SIDEBAR_LINKS'] - # Compatibility alias - self.config['SIDEBAR_LINKS'] = self.config['NAVIGATION_LINKS'] + self.config['RSS_READ_MORE_LINK'] = utils.TranslatableSetting('RSS_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS']) - if self.config['NAVIGATION_LINKS'] in (None, {}): - self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()} + # Moot.it renamed themselves to muut.io + # TODO: remove on v8? + if self.config.get('COMMENT_SYSTEM') == 'moot': + utils.LOGGER.warn('The moot comment system has been renamed to muut by the upstream. Setting COMMENT_SYSTEM to "muut".') + self.config['COMMENT_SYSTEM'] = 'muut' + + # Disable RSS. For a successful disable, we must have both the option + # false and the plugin disabled through the official means. + if 'generate_rss' in self.config['DISABLED_PLUGINS'] and self.config['GENERATE_RSS'] is True: + self.config['GENERATE_RSS'] = False - # Deprecating the ADD_THIS_BUTTONS option - # TODO: remove on v7 - if 'ADD_THIS_BUTTONS' in config: - utils.LOGGER.warn('The ADD_THIS_BUTTONS option is deprecated, use SOCIAL_BUTTONS_CODE instead.') - if not config['ADD_THIS_BUTTONS']: - utils.LOGGER.warn('Setting SOCIAL_BUTTONS_CODE to empty because ADD_THIS_BUTTONS is False.') - self.config['SOCIAL_BUTTONS_CODE'] = '' - - # STRIP_INDEX_HTML config has been replaces with STRIP_INDEXES - # Port it if only the oldef form is there - # TODO: remove on v7 - if 'STRIP_INDEX_HTML' in config and 'STRIP_INDEXES' not in config: - utils.LOGGER.warn('You should configure STRIP_INDEXES instead of STRIP_INDEX_HTML') - self.config['STRIP_INDEXES'] = config['STRIP_INDEX_HTML'] + if not self.config['GENERATE_RSS'] and 'generate_rss' not in self.config['DISABLED_PLUGINS']: + self.config['DISABLED_PLUGINS'].append('generate_rss') # PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled if self.config.get('PRETTY_URLS') and 'STRIP_INDEXES' not in config: self.config['STRIP_INDEXES'] = True if not self.config.get('COPY_SOURCES'): - self.config['HIDE_SOURCELINK'] = True - - self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS', - {self.config['DEFAULT_LANG']: ''}) - - # SITE_URL is required, but if the deprecated BLOG_URL - # is available, use it and warn - # TODO: remove on v7 - if 'SITE_URL' not in self.config: - if 'BLOG_URL' in self.config: - utils.LOGGER.warn('You should configure SITE_URL instead of BLOG_URL') - self.config['SITE_URL'] = self.config['BLOG_URL'] + self.config['SHOW_SOURCELINK'] = False self.default_lang = self.config['DEFAULT_LANG'] self.translations = self.config['TRANSLATIONS'] - locale_fallback, locale_default, locales = sanitized_locales( - self.config.get('LOCALE_FALLBACK', None), - self.config.get('LOCALE_DEFAULT', None), - self.config.get('LOCALES', {}), - self.translations) # NOQA - utils.LocaleBorg.initialize(locales, self.default_lang) + if self.configured: + locale_fallback, locale_default, locales = sanitized_locales( + self.config.get('LOCALE_FALLBACK', None), + self.config.get('LOCALE_DEFAULT', None), + self.config.get('LOCALES', {}), self.translations) + utils.LocaleBorg.initialize(locales, self.default_lang) # BASE_URL defaults to SITE_URL if 'BASE_URL' not in self.config: @@ -354,6 +483,10 @@ class Nikola(object): if self.config['BASE_URL'] and self.config['BASE_URL'][-1] != '/': utils.LOGGER.warn("Your BASE_URL doesn't end in / -- adding it.") + # We use one global tzinfo object all over Nikola. + self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE']) + self.config['__tzinfo__'] = self.tzinfo + self.plugin_manager = PluginManager(categories_filter={ "Command": Command, "Task": Task, @@ -362,19 +495,22 @@ class Nikola(object): "PageCompiler": PageCompiler, "TaskMultiplier": TaskMultiplier, "RestExtension": RestExtension, + "MarkdownExtension": MarkdownExtension, "SignalHandler": SignalHandler, }) self.plugin_manager.setPluginInfoExtension('plugin') extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS'] if sys.version_info[0] == 3: places = [ - os.path.join(os.path.dirname(__file__), 'plugins'), + resource_filename('nikola', 'plugins'), os.path.join(os.getcwd(), 'plugins'), + os.path.expanduser('~/.nikola/plugins'), ] + [path for path in extra_plugins_dirs if path] else: places = [ - os.path.join(os.path.dirname(__file__), utils.sys_encode('plugins')), + resource_filename('nikola', utils.sys_encode('plugins')), os.path.join(os.getcwd(), utils.sys_encode('plugins')), + os.path.expanduser('~/.nikola/plugins'), ] + [utils.sys_encode(path) for path in extra_plugins_dirs if path] self.plugin_manager.setPluginPlaces(places) @@ -391,26 +527,22 @@ class Nikola(object): # Emit signal for SignalHandlers which need to start running immediately. signal('sighandlers_loaded').send(self) - self.commands = {} + self._commands = {} # Activate all command plugins for plugin_info in self.plugin_manager.getPluginsOfCategory("Command"): - if (plugin_info.name in self.config['DISABLED_PLUGINS'] - or (plugin_info.name in self.EXTRA_PLUGINS and - plugin_info.name not in self.config['ENABLED_EXTRAS'])): + if plugin_info.name in self.config['DISABLED_PLUGINS']: self.plugin_manager.removePluginFromCategory(plugin_info, "Command") continue self.plugin_manager.activatePluginByName(plugin_info.name) plugin_info.plugin_object.set_site(self) plugin_info.plugin_object.short_help = plugin_info.description - self.commands[plugin_info.name] = plugin_info.plugin_object + self._commands[plugin_info.name] = plugin_info.plugin_object # Activate all task plugins for task_type in ["Task", "LateTask"]: for plugin_info in self.plugin_manager.getPluginsOfCategory(task_type): - if (plugin_info.name in self.config['DISABLED_PLUGINS'] - or (plugin_info.name in self.EXTRA_PLUGINS and - plugin_info.name not in self.config['ENABLED_EXTRAS'])): + if plugin_info.name in self.config['DISABLED_PLUGINS']: self.plugin_manager.removePluginFromCategory(plugin_info, task_type) continue self.plugin_manager.activatePluginByName(plugin_info.name) @@ -418,20 +550,24 @@ class Nikola(object): # Activate all multiplier plugins for plugin_info in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"): - if (plugin_info.name in self.config['DISABLED_PLUGINS'] - or (plugin_info.name in self.EXTRA_PLUGINS and - plugin_info.name not in self.config['ENABLED_EXTRAS'])): + if plugin_info.name in self.config['DISABLED_PLUGINS']: self.plugin_manager.removePluginFromCategory(plugin_info, task_type) continue self.plugin_manager.activatePluginByName(plugin_info.name) plugin_info.plugin_object.set_site(self) + compilers = defaultdict(set) # Also add aliases for combinations with TRANSLATIONS_PATTERN - self.config['COMPILERS'] = dict([(lang, list(exts) + [ - utils.get_translation_candidate(self.config, "f" + ext, lang)[1:] - for ext in exts - for lang in self.config['TRANSLATIONS'].keys()]) - for lang, exts in list(self.config['COMPILERS'].items())]) + for compiler, exts in self.config['COMPILERS'].items(): + for ext in exts: + compilers[compiler].add(ext) + for lang in self.config['TRANSLATIONS'].keys(): + candidate = utils.get_translation_candidate(self.config, "f" + ext, lang) + compilers[compiler].add(candidate) + + # Avoid redundant compilers + for k, v in compilers.items(): + self.config['COMPILERS'][k] = sorted(list(v)) # Activate all required compiler plugins for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"): @@ -439,11 +575,13 @@ class Nikola(object): self.plugin_manager.activatePluginByName(plugin_info.name) plugin_info.plugin_object.set_site(self) - # set global_context for template rendering - self._GLOBAL_CONTEXT = {} - + self._GLOBAL_CONTEXT['url_type'] = self.config['URL_TYPE'] + self._GLOBAL_CONTEXT['timezone'] = self.tzinfo self._GLOBAL_CONTEXT['_link'] = self.link - self._GLOBAL_CONTEXT['set_locale'] = utils.LocaleBorg().set_locale + try: + self._GLOBAL_CONTEXT['set_locale'] = utils.LocaleBorg().set_locale + except utils.LocaleBorgUninitializedException: + self._GLOBAL_CONTEXT['set_locale'] = None self._GLOBAL_CONTEXT['rel_link'] = self.rel_link self._GLOBAL_CONTEXT['abs_link'] = self.abs_link self._GLOBAL_CONTEXT['exists'] = self.file_exists @@ -458,49 +596,45 @@ class Nikola(object): 'DATE_FORMAT', '%Y-%m-%d %H:%M') self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR') self._GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE') + self._GLOBAL_CONTEXT['show_blog_title'] = self.config.get('SHOW_BLOG_TITLE') + self._GLOBAL_CONTEXT['logo_url'] = self.config.get('LOGO_URL') + self._GLOBAL_CONTEXT['blog_description'] = self.config.get('BLOG_DESCRIPTION') - # TODO: remove fallback in v7 - self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL')) + # TODO: remove in v8 self._GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION') + + self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL') + self._GLOBAL_CONTEXT['template_hooks'] = self.template_hooks self._GLOBAL_CONTEXT['body_end'] = self.config.get('BODY_END') - # TODO: remove in v7 - self._GLOBAL_CONTEXT['analytics'] = self.config.get('BODY_END') - # TODO: remove in v7 - self._GLOBAL_CONTEXT['add_this_buttons'] = self.config.get('SOCIAL_BUTTONS_CODE') self._GLOBAL_CONTEXT['social_buttons_code'] = self.config.get('SOCIAL_BUTTONS_CODE') self._GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS') self._GLOBAL_CONTEXT['license'] = self.config.get('LICENSE') self._GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM') self._GLOBAL_CONTEXT['comment_system'] = self.config.get('COMMENT_SYSTEM') self._GLOBAL_CONTEXT['comment_system_id'] = self.config.get('COMMENT_SYSTEM_ID') - # TODO: remove in v7 - self._GLOBAL_CONTEXT['disqus_forum'] = self.config.get('COMMENT_SYSTEM_ID') + self._GLOBAL_CONTEXT['site_has_comments'] = bool(self.config.get('COMMENT_SYSTEM')) self._GLOBAL_CONTEXT['mathjax_config'] = self.config.get( 'MATHJAX_CONFIG') self._GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONFIG_SUBTHEME') self._GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONFIG_TRANSITION') self._GLOBAL_CONTEXT['content_footer'] = self.config.get( 'CONTENT_FOOTER') + self._GLOBAL_CONTEXT['generate_rss'] = self.config.get('GENERATE_RSS') self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH') self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK') - self._GLOBAL_CONTEXT['navigation_links'] = utils.Functionary(list, self.config['DEFAULT_LANG']) - for k, v in self.config.get('NAVIGATION_LINKS', {}).items(): - self._GLOBAL_CONTEXT['navigation_links'][k] = v - - # avoid #1082 by making sure all keys in navigation_links are read once - for k in self._GLOBAL_CONTEXT['translations']: - self._GLOBAL_CONTEXT['navigation_links'][k] - - # TODO: remove on v7 - # Compatibility alias - self._GLOBAL_CONTEXT['sidebar_links'] = self._GLOBAL_CONTEXT['navigation_links'] + self._GLOBAL_CONTEXT['navigation_links'] = self.config.get('NAVIGATION_LINKS') + self._GLOBAL_CONTEXT['use_open_graph'] = self.config.get( + 'USE_OPEN_GRAPH', True) self._GLOBAL_CONTEXT['twitter_card'] = self.config.get( 'TWITTER_CARD', {}) - self._GLOBAL_CONTEXT['hide_sourcelink'] = self.config.get( - 'HIDE_SOURCELINK') + self._GLOBAL_CONTEXT['hide_sourcelink'] = not self.config.get( + 'SHOW_SOURCELINK') + self._GLOBAL_CONTEXT['show_sourcelink'] = self.config.get( + 'SHOW_SOURCELINK') self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA') + self._GLOBAL_CONTEXT['colorbox_locales'] = LEGAL_VALUES['COLORBOX_LOCALES'] self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {})) @@ -512,29 +646,15 @@ class Nikola(object): "PageCompiler"): self.compilers[plugin_info.name] = \ plugin_info.plugin_object + signal('configured').send(self) def _get_themes(self): if self._THEMES is None: - # Check for old theme names (Issue #650) TODO: remove in v7 - theme_replacements = { - 'site': 'bootstrap', - 'orphan': 'base', - 'default': 'oldfashioned', - } - if self.config['THEME'] in theme_replacements: - utils.LOGGER.warn('You are using the old theme "{0}", using "{1}" instead.'.format( - self.config['THEME'], theme_replacements[self.config['THEME']])) - self.config['THEME'] = theme_replacements[self.config['THEME']] - if self.config['THEME'] == 'oldfashioned': - utils.LOGGER.warn('''You may need to install the "oldfashioned" theme ''' - '''from themes.getnikola.com because it's not ''' - '''shipped by default anymore.''') - utils.LOGGER.warn('Please change your THEME setting.') try: self._THEMES = utils.get_theme_chain(self.config['THEME']) except Exception: - utils.LOGGER.warn('''Can't load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME'])) + utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME'])) self.config['THEME'] = 'bootstrap' return self._get_themes() # Check consistency of USE_CDN and the current THEME (Issue #386) @@ -549,9 +669,13 @@ class Nikola(object): THEMES = property(_get_themes) def _get_messages(self): - return utils.load_messages(self.THEMES, - self.translations, - self.default_lang) + try: + return utils.load_messages(self.THEMES, + self.translations, + self.default_lang) + except utils.LanguageNotFoundError as e: + utils.LOGGER.error('''Cannot load language "{0}". Please make sure it is supported by Nikola itself, or that you have the appropriate messages files in your themes.'''.format(e.lang)) + sys.exit(1) MESSAGES = property(_get_messages) @@ -633,8 +757,14 @@ class Nikola(object): local_context["template_name"] = template_name local_context.update(self.GLOBAL_CONTEXT) local_context.update(context) + for k in self._GLOBAL_CONTEXT_TRANSLATABLE: + local_context[k] = local_context[k](local_context['lang']) + local_context['is_rtl'] = local_context['lang'] in LEGAL_VALUES['RTL_LANGUAGES'] # string, arguments local_context["formatmsg"] = lambda s, *a: s % a + for h in local_context['template_hooks'].values(): + h.context = context + data = self.template_system.render_template( template_name, None, local_context) @@ -652,7 +782,7 @@ class Nikola(object): utils.makedirs(os.path.dirname(output_name)) doc = lxml.html.document_fromstring(data) doc.rewrite_links(lambda dst: self.url_replacer(src, dst, context['lang'])) - data = b'<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8') + data = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True) with open(output_name, "wb+") as post_file: post_file.write(data) @@ -745,30 +875,44 @@ class Nikola(object): return result def generic_rss_renderer(self, lang, title, link, description, timeline, output_path, - rss_teasers, feed_length=10, feed_url=None): + rss_teasers, rss_plain, feed_length=10, feed_url=None, enclosure=None): + """Takes all necessary data, and renders a RSS feed in output_path.""" + rss_obj = rss.RSS2( + title=title, + link=link, + description=description, + lastBuildDate=datetime.datetime.now(), + generator='http://getnikola.com/', + language=lang + ) + items = [] + for post in timeline[:feed_length]: - # Massage the post's HTML - data = post.text(lang, teaser_only=rss_teasers, really_absolute=True) + old_url_type = self.config['URL_TYPE'] + self.config['URL_TYPE'] = 'absolute' + data = post.text(lang, teaser_only=rss_teasers, strip_html=rss_plain, rss_read_more_link=True) if feed_url is not None and data: - # FIXME: this is duplicated with code in Post.text() - try: - doc = lxml.html.document_fromstring(data) - doc.rewrite_links(lambda dst: self.url_replacer(feed_url, dst, lang)) + # Massage the post's HTML (unless plain) + if not rss_plain: + # FIXME: this is duplicated with code in Post.text() try: - body = doc.body - data = (body.text or '') + ''.join( - [lxml.html.tostring(child, encoding='unicode') - for child in body.iterchildren()]) - except IndexError: # No body there, it happens sometimes - data = '' - except lxml.etree.ParserError as e: - if str(e) == "Document is empty": - data = "" - else: # let other errors raise - raise(e) - + doc = lxml.html.document_fromstring(data) + doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang)) + try: + body = doc.body + data = (body.text or '') + ''.join( + [lxml.html.tostring(child, encoding='unicode') + for child in body.iterchildren()]) + except IndexError: # No body there, it happens sometimes + data = '' + except lxml.etree.ParserError as e: + if str(e) == "Document is empty": + data = "" + else: # let other errors raise + raise(e) + self.config['URL_TYPE'] = old_url_type args = { 'title': post.title(lang), 'link': post.permalink(lang, absolute=True), @@ -776,24 +920,28 @@ class Nikola(object): 'guid': post.permalink(lang, absolute=True), # PyRSS2Gen's pubDate is GMT time. 'pubDate': (post.date if post.date.tzinfo is None else - post.date.astimezone(pytz.timezone('UTC'))), + post.date.astimezone(dateutil.tz.tzutc())), 'categories': post._tags.get(lang, []), - 'author': post.meta('author'), + 'creator': post.author(lang), } + if post.author(lang): + rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" + + """ Enclosure callback must returns tuple """ + if enclosure: + download_link, download_size, download_type = enclosure(post=post, lang=lang) + + args['enclosure'] = rss.Enclosure( + download_link, + download_size, + download_type, + ) + items.append(utils.ExtendedItem(**args)) - rss_obj = utils.ExtendedRSS2( - title=title, - link=link, - description=description, - lastBuildDate=datetime.datetime.now(), - items=items, - generator='Nikola <http://getnikola.com/>', - language=lang - ) - rss_obj.self_url = feed_url - rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" - rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" + + rss_obj.items = items + dst_dir = os.path.dirname(output_path) utils.makedirs(dst_dir) with codecs.open(output_path, "wb+", "utf-8") as rss_file: @@ -838,19 +986,23 @@ class Nikola(object): if lang is None: lang = utils.LocaleBorg().current_lang - path = self.path_handlers[kind](name, lang) - path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028 - - if is_link: - link = '/' + ('/'.join(path)) - index_len = len(self.config['INDEX_FILE']) - if self.config['STRIP_INDEXES'] and \ - link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']: - return link[:-index_len] + try: + path = self.path_handlers[kind](name, lang) + path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028 + + if is_link: + link = '/' + ('/'.join(path)) + index_len = len(self.config['INDEX_FILE']) + if self.config['STRIP_INDEXES'] and \ + link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']: + return link[:-index_len] + else: + return link else: - return link - else: - return os.path.join(*path) + return os.path.join(*path) + except KeyError: + utils.LOGGER.warn("Unknown path request of kind: {0}".format(kind)) + return "" def post_path(self, name, lang): """post_path path handler""" @@ -862,7 +1014,7 @@ class Nikola(object): """slug path handler""" results = [p for p in self.timeline if p.meta('slug') == name] if not results: - utils.LOGGER.warning("Can't resolve path request for slug: {0}".format(name)) + utils.LOGGER.warning("Cannot resolve path request for slug: {0}".format(name)) else: if len(results) > 1: utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name)) @@ -872,7 +1024,7 @@ class Nikola(object): """filename path handler""" results = [p for p in self.timeline if p.source_path == name] if not results: - utils.LOGGER.warning("Can't resolve path request for filename: {0}".format(name)) + utils.LOGGER.warning("Cannot resolve path request for filename: {0}".format(name)) else: if len(results) > 1: utils.LOGGER.error("Ambiguous path request for filename: {0}".format(name)) @@ -887,13 +1039,16 @@ class Nikola(object): def link(self, *args): return self.path(*args, is_link=True) - def abs_link(self, dst): + def abs_link(self, dst, protocol_relative=False): # Normalize if dst: # Mako templates and empty strings evaluate to False dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) else: dst = self.config['BASE_URL'] - return urlparse(dst).geturl() + url = urlparse(dst).geturl() + if protocol_relative: + url = url.split(":", 1)[1] + return url def rel_link(self, src, dst): # Normalize @@ -967,40 +1122,55 @@ class Nikola(object): 'task_dep': task_dep } - def scan_posts(self): + def scan_posts(self, really=False): """Scan all the posts.""" - if self._scanned: + if self._scanned and not really: return + + self.commands = utils.Commands(self.doit) + self.global_data = {} + self.posts = [] + self.posts_per_year = defaultdict(list) + self.posts_per_month = defaultdict(list) + self.posts_per_tag = defaultdict(list) + self.posts_per_category = defaultdict(list) + self.post_per_file = {} + self.timeline = [] + self.pages = [] + seen = set([]) - print("Scanning posts", end='', file=sys.stderr) + if not self.quiet: + print("Scanning posts", end='', file=sys.stderr) slugged_tags = set([]) quit = False for wildcard, destination, template_name, use_in_feeds in \ self.config['post_pages']: - print(".", end='', file=sys.stderr) + if not self.quiet: + print(".", end='', file=sys.stderr) dirname = os.path.dirname(wildcard) - for dirpath, _, _ in os.walk(dirname): - dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) + for dirpath, _, _ in os.walk(dirname, followlinks=True): dest_dir = os.path.normpath(os.path.join(destination, - os.path.relpath(dirpath, dirname))) - full_list = glob.glob(dir_glob) - # Now let's look for things that are not in default_lang + os.path.relpath(dirpath, dirname))) # output/destination/foo/ + # Get all the untranslated paths + dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst + untranslated = glob.glob(dir_glob) + # And now get all the translated paths + translated = set([]) for lang in self.config['TRANSLATIONS'].keys(): - lang_glob = utils.get_translation_candidate(self.config, dir_glob, lang) - translated_list = glob.glob(lang_glob) - # dir_glob could have put it already in full_list - full_list = list(set(full_list + translated_list)) - - # Eliminate translations from full_list if they are not the primary, - # or a secondary with no primary - limited_list = full_list[:] - for fname in full_list: - for lang in self.config['TRANSLATIONS'].keys(): - translation = utils.get_translation_candidate(self.config, fname, lang) - if translation in full_list: - limited_list.remove(translation) - full_list = limited_list - + if lang == self.config['DEFAULT_LANG']: + continue + lang_glob = utils.get_translation_candidate(self.config, dir_glob, lang) # posts/foo/*.LANG.rst + translated = translated.union(set(glob.glob(lang_glob))) + # untranslated globs like *.rst often match translated paths too, so remove them + # and ensure x.rst is not in the translated set + untranslated = set(untranslated) - translated + + # also remove from translated paths that are translations of + # paths in untranslated_list, so x.es.rst is not in the untranslated set + for p in untranslated: + translated = translated - set([utils.get_translation_candidate(self.config, p, l) for l in self.config['TRANSLATIONS'].keys()]) + + full_list = list(translated) + list(untranslated) # We eliminate from the list the files inside any .ipynb folder full_list = [p for p in full_list if not any([x.startswith('.') @@ -1020,13 +1190,14 @@ class Nikola(object): template_name, self.get_compiler(base_path) ) + self.timeline.append(post) self.global_data[post.source_path] = post if post.use_in_feeds: - self.posts.append(post.source_path) + self.posts.append(post) self.posts_per_year[ - str(post.date.year)].append(post.source_path) + str(post.date.year)].append(post) self.posts_per_month[ - '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.source_path) + '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post) for tag in post.alltags: if utils.slugify(tag) in slugged_tags: if tag not in self.posts_per_tag: @@ -1034,28 +1205,35 @@ class Nikola(object): other_tag = [k for k in self.posts_per_tag.keys() if k.lower() == tag.lower()][0] utils.LOGGER.error('You have tags that are too similar: {0} and {1}'.format(tag, other_tag)) utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path)) - utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join(self.posts_per_tag[other_tag]))) + utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join([p.source_path for p in self.posts_per_tag[other_tag]]))) quit = True else: slugged_tags.add(utils.slugify(tag)) - self.posts_per_tag[tag].append(post.source_path) - self.posts_per_category[post.meta('category')].append(post.source_path) + self.posts_per_tag[tag].append(post) + self.posts_per_category[post.meta('category')].append(post) else: self.pages.append(post) self.post_per_file[post.destination_path(lang=lang)] = post self.post_per_file[post.destination_path(lang=lang, extension=post.source_ext())] = post - for name, post in list(self.global_data.items()): - self.timeline.append(post) + # Sort everything. self.timeline.sort(key=lambda p: p.date) self.timeline.reverse() - post_timeline = [p for p in self.timeline if p.use_in_feeds] - for i, p in enumerate(post_timeline[1:]): - p.next_post = post_timeline[i] - for i, p in enumerate(post_timeline[:-1]): - p.prev_post = post_timeline[i + 1] + self.posts.sort(key=lambda p: p.date) + self.posts.reverse() + self.pages.sort(key=lambda p: p.date) + self.pages.reverse() + + for i, p in enumerate(self.posts[1:]): + p.next_post = self.posts[i] + for i, p in enumerate(self.posts[:-1]): + p.prev_post = self.posts[i + 1] self._scanned = True - print("done!", file=sys.stderr) + if not self.quiet: + print("done!", file=sys.stderr) + + signal('scanned').send(self) + if quit: sys.exit(1) @@ -1089,6 +1267,13 @@ class Nikola(object): deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS'] deps_dict['global'] = self.GLOBAL_CONTEXT deps_dict['comments'] = context['enable_comments'] + + for k, v in self.GLOBAL_CONTEXT['template_hooks'].items(): + deps_dict['||template_hooks|{0}||'.format(k)] = v._items + + for k in self._GLOBAL_CONTEXT_TRANSLATABLE: + deps_dict[k] = deps_dict['global'][k](lang) + if post: deps_dict['post_translations'] = post.translated_to @@ -1113,8 +1298,8 @@ class Nikola(object): deps += post.deps(lang) context = {} context["posts"] = posts - context["title"] = self.config['BLOG_TITLE'] - context["description"] = self.config['BLOG_DESCRIPTION'] + context["title"] = self.config['BLOG_TITLE'](lang) + context["description"] = self.config['BLOG_DESCRIPTION'](lang) context["lang"] = lang context["prevlink"] = None context["nextlink"] = None @@ -1123,6 +1308,13 @@ class Nikola(object): deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in posts] deps_context["global"] = self.GLOBAL_CONTEXT + + for k, v in self.GLOBAL_CONTEXT['template_hooks'].items(): + deps_context['||template_hooks|{0}||'.format(k)] = v._items + + for k in self._GLOBAL_CONTEXT_TRANSLATABLE: + deps_context[k] = deps_context['global'][k](lang) + task = { 'name': os.path.normpath(output_name), 'targets': [output_name], @@ -1135,6 +1327,9 @@ class Nikola(object): return utils.apply_filters(task, filters) + def __repr__(self): + return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE']()) + def sanitized_locales(locale_fallback, locale_default, locales, translations): """Sanitizes all locales availble into a nikola session @@ -1297,7 +1492,7 @@ _windows_locale_guesses = { "en": "English", "eo": "Esperanto", "es": "Spanish", - "fa": "Farsi", # persian + "fa": "Farsi", # Persian "fr": "French", "hr": "Croatian", "it": "Italian", @@ -1322,6 +1517,6 @@ SOCIAL_BUTTONS_CODE = """ <li><a class="addthis_button_twitter"></a> </ul> </div> -<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> +<script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> <!-- End of social buttons --> """ diff --git a/nikola/packages/README.md b/nikola/packages/README.md new file mode 100644 index 0000000..156d43f --- /dev/null +++ b/nikola/packages/README.md @@ -0,0 +1,5 @@ +We ship some third-party things with Nikola. They live here, along with their licenses. + +Packages: + + * tzlocal by Lennart Regebro, CC0 license (modified) diff --git a/nikola/packages/__init__.py b/nikola/packages/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/packages/__init__.py diff --git a/nikola/packages/tzlocal/LICENSE.txt b/nikola/packages/tzlocal/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/nikola/packages/tzlocal/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/nikola/packages/tzlocal/__init__.py b/nikola/packages/tzlocal/__init__.py new file mode 100644 index 0000000..10716ec --- /dev/null +++ b/nikola/packages/tzlocal/__init__.py @@ -0,0 +1,7 @@ +import sys +if sys.platform == 'win32': + from .win32 import get_localzone, reload_localzone # NOQA +elif 'darwin' in sys.platform: + from .darwin import get_localzone, reload_localzone # NOQA +else: + from .unix import get_localzone, reload_localzone # NOQA diff --git a/nikola/packages/tzlocal/darwin.py b/nikola/packages/tzlocal/darwin.py new file mode 100644 index 0000000..8aeee51 --- /dev/null +++ b/nikola/packages/tzlocal/darwin.py @@ -0,0 +1,33 @@ +from __future__ import with_statement +import os +import dateutil.tz + +_cache_tz = None + + +def _get_localzone(): + tzname = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() + if not tzname: + # link will be something like /usr/share/zoneinfo/America/Los_Angeles. + link = os.readlink("/etc/localtime") + tzname = link[link.rfind('/', 0, link.rfind('/')) + 1:] + try: + dateutil.tz.gettz(tzname) + return tzname + except: + return None + + +def get_localzone(): + """Get the computers configured local timezone, if any.""" + global _cache_tz + if _cache_tz is None: + _cache_tz = _get_localzone() + return _cache_tz + + +def reload_localzone(): + """Reload the cached localzone. You need to call this if the timezone has changed.""" + global _cache_tz + _cache_tz = _get_localzone() + return _cache_tz diff --git a/nikola/packages/tzlocal/unix.py b/nikola/packages/tzlocal/unix.py new file mode 100644 index 0000000..8e913b8 --- /dev/null +++ b/nikola/packages/tzlocal/unix.py @@ -0,0 +1,126 @@ +from __future__ import with_statement +import os +import re +import dateutil.tz + +_cache_tz = None + + +def _get_localzone(): + """Tries to find the local timezone configuration. + + This method prefers finding the timezone name and passing that to pytz, + over passing in the localtime file, as in the later case the zoneinfo + name is unknown. + + The parameter _root makes the function look for files like /etc/localtime + beneath the _root directory. This is primarily used by the tests. + In normal usage you call the function without parameters.""" + + tz = os.environ.get('TZ') + if tz and tz[0] == ':': + tz = tz[1:] + try: + if tz: + dateutil.tz.gettz(tz) + return tz + except: + pass + + try: + # link will be something like /usr/share/zoneinfo/America/Los_Angeles. + link = os.readlink('/etc/localtime') + tz = link[link.rfind('/', 0, link.rfind('/')) + 1:] + + if tz: + dateutil.tz.gettz(tz) + return tz + except: + return None + + # Now look for distribution specific configuration files + # that contain the timezone name. + tzpath = os.path.join('/etc/timezone') + if os.path.exists(tzpath): + with open(tzpath, 'rb') as tzfile: + data = tzfile.read() + + # Issue #3 was that /etc/timezone was a zoneinfo file. + # That's a misconfiguration, but we need to handle it gracefully: + if data[:5] != 'TZif2': + etctz = data.strip().decode() + # Get rid of host definitions and comments: + if ' ' in etctz: + etctz, dummy = etctz.split(' ', 1) + if '#' in etctz: + etctz, dummy = etctz.split('#', 1) + tz = etctz.replace(' ', '_') + try: + if tz: + dateutil.tz.gettz(tz) + return tz + except: + pass + + # CentOS has a ZONE setting in /etc/sysconfig/clock, + # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and + # Gentoo has a TIMEZONE setting in /etc/conf.d/clock + # We look through these files for a timezone: + + zone_re = re.compile('\s*ZONE\s*=\s*\"') + timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"') + end_re = re.compile('\"') + + for tzpath in ('/etc/sysconfig/clock', '/etc/conf.d/clock'): + if not os.path.exists(tzpath): + continue + with open(tzpath, 'rt') as tzfile: + data = tzfile.readlines() + + for line in data: + # Look for the ZONE= setting. + match = zone_re.match(line) + if match is None: + # No ZONE= setting. Look for the TIMEZONE= setting. + match = timezone_re.match(line) + if match is not None: + # Some setting existed + line = line[match.end():] + etctz = line[:end_re.search(line).start()] + + # We found a timezone + tz = etctz.replace(' ', '_') + try: + if tz: + dateutil.tz.gettz(tz) + return tz + except: + pass + + # Nikola cannot use this thing below... + + # No explicit setting existed. Use localtime + # for filename in ('etc/localtime', 'usr/local/etc/localtime'): + # tzpath = os.path.join(_root, filename) + + # if not os.path.exists(tzpath): + # continue + # with open(tzpath, 'rb') as tzfile: + # return pytz.tzfile.build_tzinfo('local', tzfile) + + return None + + +def get_localzone(): + """Get the computers configured local timezone, if any.""" + global _cache_tz + if _cache_tz is None: + _cache_tz = _get_localzone() + return _cache_tz + + +def reload_localzone(): + """Reload the cached localzone. You need to call this if the timezone has changed.""" + global _cache_tz + _cache_tz = _get_localzone() + return _cache_tz diff --git a/nikola/packages/tzlocal/win32.py b/nikola/packages/tzlocal/win32.py new file mode 100644 index 0000000..7005422 --- /dev/null +++ b/nikola/packages/tzlocal/win32.py @@ -0,0 +1,92 @@ +try: + import _winreg as winreg +except ImportError: + try: + import winreg + except ImportError: + pass # not windows + +from .windows_tz import win_tz + +_cache_tz = None + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dict = {} + size = winreg.QueryInfoKey(key)[1] + for i in range(size): + data = winreg.EnumValue(key, i) + dict[data[0]] = data[1] + return dict + + +def get_localzone_name(): + # Windows is special. It has unique time zone names (in several + # meanings of the word) available, but unfortunately, they can be + # translated to the language of the operating system, so we need to + # do a backwards lookup, by going through all time zones and see which + # one matches. + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + + TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + localtz = winreg.OpenKey(handle, TZLOCALKEYNAME) + keyvalues = valuestodict(localtz) + localtz.Close() + if 'TimeZoneKeyName' in keyvalues: + # Windows 7 (and Vista?) + + # For some reason this returns a string with loads of NUL bytes at + # least on some systems. I don't know if this is a bug somewhere, I + # just work around it. + tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0] + else: + # Windows 2000 or XP + + # This is the localized name: + tzwin = keyvalues['StandardName'] + + # Open the list of timezones to look up the real name: + TZKEYNAME = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" + tzkey = winreg.OpenKey(handle, TZKEYNAME) + + # Now, match this value to Time Zone information + tzkeyname = None + for i in range(winreg.QueryInfoKey(tzkey)[0]): + subkey = winreg.EnumKey(tzkey, i) + sub = winreg.OpenKey(tzkey, subkey) + data = valuestodict(sub) + sub.Close() + if data['Std'] == tzwin: + tzkeyname = subkey + break + + tzkey.Close() + handle.Close() + + if tzkeyname is None: + raise LookupError('Can not find Windows timezone configuration') + + timezone = win_tz.get(tzkeyname) + if timezone is None: + # Nope, that didn't work. Try adding "Standard Time", + # it seems to work a lot of times: + timezone = win_tz.get(tzkeyname + " Standard Time") + + # Return what we have. + return timezone + + +def get_localzone(): + """Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone.""" + global _cache_tz + if _cache_tz is None: + _cache_tz = get_localzone_name() + return _cache_tz + + +def reload_localzone(): + """Reload the cached localzone. You need to call this if the timezone has changed.""" + global _cache_tz + _cache_tz = get_localzone_name() + return _cache_tz diff --git a/nikola/packages/tzlocal/windows_tz.py b/nikola/packages/tzlocal/windows_tz.py new file mode 100644 index 0000000..1084478 --- /dev/null +++ b/nikola/packages/tzlocal/windows_tz.py @@ -0,0 +1,543 @@ +# This file is autogenerated by the get_windows_info.py script +# Do not edit. +win_tz = { + 'AUS Central Standard Time': 'Australia/Darwin', + 'AUS Eastern Standard Time': 'Australia/Sydney', + 'Afghanistan Standard Time': 'Asia/Kabul', + 'Alaskan Standard Time': 'America/Anchorage', + 'Arab Standard Time': 'Asia/Riyadh', + 'Arabian Standard Time': 'Asia/Dubai', + 'Arabic Standard Time': 'Asia/Baghdad', + 'Argentina Standard Time': 'America/Buenos_Aires', + 'Atlantic Standard Time': 'America/Halifax', + 'Azerbaijan Standard Time': 'Asia/Baku', + 'Azores Standard Time': 'Atlantic/Azores', + 'Bahia Standard Time': 'America/Bahia', + 'Bangladesh Standard Time': 'Asia/Dhaka', + 'Canada Central Standard Time': 'America/Regina', + 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', + 'Caucasus Standard Time': 'Asia/Yerevan', + 'Cen. Australia Standard Time': 'Australia/Adelaide', + 'Central America Standard Time': 'America/Guatemala', + 'Central Asia Standard Time': 'Asia/Almaty', + 'Central Brazilian Standard Time': 'America/Cuiaba', + 'Central Europe Standard Time': 'Europe/Budapest', + 'Central European Standard Time': 'Europe/Warsaw', + 'Central Pacific Standard Time': 'Pacific/Guadalcanal', + 'Central Standard Time': 'America/Chicago', + 'Central Standard Time (Mexico)': 'America/Mexico_City', + 'China Standard Time': 'Asia/Shanghai', + 'Dateline Standard Time': 'Etc/GMT+12', + 'E. Africa Standard Time': 'Africa/Nairobi', + 'E. Australia Standard Time': 'Australia/Brisbane', + 'E. Europe Standard Time': 'Asia/Nicosia', + 'E. South America Standard Time': 'America/Sao_Paulo', + 'Eastern Standard Time': 'America/New_York', + 'Egypt Standard Time': 'Africa/Cairo', + 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', + 'FLE Standard Time': 'Europe/Kiev', + 'Fiji Standard Time': 'Pacific/Fiji', + 'GMT Standard Time': 'Europe/London', + 'GTB Standard Time': 'Europe/Bucharest', + 'Georgian Standard Time': 'Asia/Tbilisi', + 'Greenland Standard Time': 'America/Godthab', + 'Greenwich Standard Time': 'Atlantic/Reykjavik', + 'Hawaiian Standard Time': 'Pacific/Honolulu', + 'India Standard Time': 'Asia/Calcutta', + 'Iran Standard Time': 'Asia/Tehran', + 'Israel Standard Time': 'Asia/Jerusalem', + 'Jordan Standard Time': 'Asia/Amman', + 'Kaliningrad Standard Time': 'Europe/Kaliningrad', + 'Korea Standard Time': 'Asia/Seoul', + 'Libya Standard Time': 'Africa/Tripoli', + 'Magadan Standard Time': 'Asia/Magadan', + 'Mauritius Standard Time': 'Indian/Mauritius', + 'Middle East Standard Time': 'Asia/Beirut', + 'Montevideo Standard Time': 'America/Montevideo', + 'Morocco Standard Time': 'Africa/Casablanca', + 'Mountain Standard Time': 'America/Denver', + 'Mountain Standard Time (Mexico)': 'America/Chihuahua', + 'Myanmar Standard Time': 'Asia/Rangoon', + 'N. Central Asia Standard Time': 'Asia/Novosibirsk', + 'Namibia Standard Time': 'Africa/Windhoek', + 'Nepal Standard Time': 'Asia/Katmandu', + 'New Zealand Standard Time': 'Pacific/Auckland', + 'Newfoundland Standard Time': 'America/St_Johns', + 'North Asia East Standard Time': 'Asia/Irkutsk', + 'North Asia Standard Time': 'Asia/Krasnoyarsk', + 'Pacific SA Standard Time': 'America/Santiago', + 'Pacific Standard Time': 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)': 'America/Santa_Isabel', + 'Pakistan Standard Time': 'Asia/Karachi', + 'Paraguay Standard Time': 'America/Asuncion', + 'Romance Standard Time': 'Europe/Paris', + 'Russian Standard Time': 'Europe/Moscow', + 'SA Eastern Standard Time': 'America/Cayenne', + 'SA Pacific Standard Time': 'America/Bogota', + 'SA Western Standard Time': 'America/La_Paz', + 'SE Asia Standard Time': 'Asia/Bangkok', + 'Samoa Standard Time': 'Pacific/Apia', + 'Singapore Standard Time': 'Asia/Singapore', + 'South Africa Standard Time': 'Africa/Johannesburg', + 'Sri Lanka Standard Time': 'Asia/Colombo', + 'Syria Standard Time': 'Asia/Damascus', + 'Taipei Standard Time': 'Asia/Taipei', + 'Tasmania Standard Time': 'Australia/Hobart', + 'Tokyo Standard Time': 'Asia/Tokyo', + 'Tonga Standard Time': 'Pacific/Tongatapu', + 'Turkey Standard Time': 'Europe/Istanbul', + 'US Eastern Standard Time': 'America/Indianapolis', + 'US Mountain Standard Time': 'America/Phoenix', + 'UTC': 'Etc/GMT', + 'UTC+12': 'Etc/GMT-12', + 'UTC-02': 'Etc/GMT+2', + 'UTC-11': 'Etc/GMT+11', + 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', + 'Venezuela Standard Time': 'America/Caracas', + 'Vladivostok Standard Time': 'Asia/Vladivostok', + 'W. Australia Standard Time': 'Australia/Perth', + 'W. Central Africa Standard Time': 'Africa/Lagos', + 'W. Europe Standard Time': 'Europe/Berlin', + 'West Asia Standard Time': 'Asia/Tashkent', + 'West Pacific Standard Time': 'Pacific/Port_Moresby', + 'Yakutsk Standard Time': 'Asia/Yakutsk' +} + +# Old name for the win_tz variable: +tz_names = win_tz + +tz_win = { + 'Africa/Abidjan': 'Greenwich Standard Time', + 'Africa/Accra': 'Greenwich Standard Time', + 'Africa/Addis_Ababa': 'E. Africa Standard Time', + 'Africa/Algiers': 'W. Central Africa Standard Time', + 'Africa/Asmera': 'E. Africa Standard Time', + 'Africa/Bamako': 'Greenwich Standard Time', + 'Africa/Bangui': 'W. Central Africa Standard Time', + 'Africa/Banjul': 'Greenwich Standard Time', + 'Africa/Bissau': 'Greenwich Standard Time', + 'Africa/Blantyre': 'South Africa Standard Time', + 'Africa/Brazzaville': 'W. Central Africa Standard Time', + 'Africa/Bujumbura': 'South Africa Standard Time', + 'Africa/Cairo': 'Egypt Standard Time', + 'Africa/Casablanca': 'Morocco Standard Time', + 'Africa/Ceuta': 'Romance Standard Time', + 'Africa/Conakry': 'Greenwich Standard Time', + 'Africa/Dakar': 'Greenwich Standard Time', + 'Africa/Dar_es_Salaam': 'E. Africa Standard Time', + 'Africa/Djibouti': 'E. Africa Standard Time', + 'Africa/Douala': 'W. Central Africa Standard Time', + 'Africa/El_Aaiun': 'Morocco Standard Time', + 'Africa/Freetown': 'Greenwich Standard Time', + 'Africa/Gaborone': 'South Africa Standard Time', + 'Africa/Harare': 'South Africa Standard Time', + 'Africa/Johannesburg': 'South Africa Standard Time', + 'Africa/Juba': 'E. Africa Standard Time', + 'Africa/Kampala': 'E. Africa Standard Time', + 'Africa/Khartoum': 'E. Africa Standard Time', + 'Africa/Kigali': 'South Africa Standard Time', + 'Africa/Kinshasa': 'W. Central Africa Standard Time', + 'Africa/Lagos': 'W. Central Africa Standard Time', + 'Africa/Libreville': 'W. Central Africa Standard Time', + 'Africa/Lome': 'Greenwich Standard Time', + 'Africa/Luanda': 'W. Central Africa Standard Time', + 'Africa/Lubumbashi': 'South Africa Standard Time', + 'Africa/Lusaka': 'South Africa Standard Time', + 'Africa/Malabo': 'W. Central Africa Standard Time', + 'Africa/Maputo': 'South Africa Standard Time', + 'Africa/Maseru': 'South Africa Standard Time', + 'Africa/Mbabane': 'South Africa Standard Time', + 'Africa/Mogadishu': 'E. Africa Standard Time', + 'Africa/Monrovia': 'Greenwich Standard Time', + 'Africa/Nairobi': 'E. Africa Standard Time', + 'Africa/Ndjamena': 'W. Central Africa Standard Time', + 'Africa/Niamey': 'W. Central Africa Standard Time', + 'Africa/Nouakchott': 'Greenwich Standard Time', + 'Africa/Ouagadougou': 'Greenwich Standard Time', + 'Africa/Porto-Novo': 'W. Central Africa Standard Time', + 'Africa/Sao_Tome': 'Greenwich Standard Time', + 'Africa/Tripoli': 'Libya Standard Time', + 'Africa/Tunis': 'W. Central Africa Standard Time', + 'Africa/Windhoek': 'Namibia Standard Time', + 'America/Anchorage': 'Alaskan Standard Time', + 'America/Anguilla': 'SA Western Standard Time', + 'America/Antigua': 'SA Western Standard Time', + 'America/Araguaina': 'SA Eastern Standard Time', + 'America/Argentina/La_Rioja': 'Argentina Standard Time', + 'America/Argentina/Rio_Gallegos': 'Argentina Standard Time', + 'America/Argentina/Salta': 'Argentina Standard Time', + 'America/Argentina/San_Juan': 'Argentina Standard Time', + 'America/Argentina/San_Luis': 'Argentina Standard Time', + 'America/Argentina/Tucuman': 'Argentina Standard Time', + 'America/Argentina/Ushuaia': 'Argentina Standard Time', + 'America/Aruba': 'SA Western Standard Time', + 'America/Asuncion': 'Paraguay Standard Time', + 'America/Bahia': 'Bahia Standard Time', + 'America/Bahia_Banderas': 'Central Standard Time (Mexico)', + 'America/Barbados': 'SA Western Standard Time', + 'America/Belem': 'SA Eastern Standard Time', + 'America/Belize': 'Central America Standard Time', + 'America/Blanc-Sablon': 'SA Western Standard Time', + 'America/Boa_Vista': 'SA Western Standard Time', + 'America/Bogota': 'SA Pacific Standard Time', + 'America/Boise': 'Mountain Standard Time', + 'America/Buenos_Aires': 'Argentina Standard Time', + 'America/Cambridge_Bay': 'Mountain Standard Time', + 'America/Campo_Grande': 'Central Brazilian Standard Time', + 'America/Cancun': 'Central Standard Time (Mexico)', + 'America/Caracas': 'Venezuela Standard Time', + 'America/Catamarca': 'Argentina Standard Time', + 'America/Cayenne': 'SA Eastern Standard Time', + 'America/Cayman': 'SA Pacific Standard Time', + 'America/Chicago': 'Central Standard Time', + 'America/Chihuahua': 'Mountain Standard Time (Mexico)', + 'America/Coral_Harbour': 'SA Pacific Standard Time', + 'America/Cordoba': 'Argentina Standard Time', + 'America/Costa_Rica': 'Central America Standard Time', + 'America/Creston': 'US Mountain Standard Time', + 'America/Cuiaba': 'Central Brazilian Standard Time', + 'America/Curacao': 'SA Western Standard Time', + 'America/Danmarkshavn': 'UTC', + 'America/Dawson': 'Pacific Standard Time', + 'America/Dawson_Creek': 'US Mountain Standard Time', + 'America/Denver': 'Mountain Standard Time', + 'America/Detroit': 'Eastern Standard Time', + 'America/Dominica': 'SA Western Standard Time', + 'America/Edmonton': 'Mountain Standard Time', + 'America/Eirunepe': 'SA Pacific Standard Time', + 'America/El_Salvador': 'Central America Standard Time', + 'America/Fortaleza': 'SA Eastern Standard Time', + 'America/Glace_Bay': 'Atlantic Standard Time', + 'America/Godthab': 'Greenland Standard Time', + 'America/Goose_Bay': 'Atlantic Standard Time', + 'America/Grand_Turk': 'Eastern Standard Time', + 'America/Grenada': 'SA Western Standard Time', + 'America/Guadeloupe': 'SA Western Standard Time', + 'America/Guatemala': 'Central America Standard Time', + 'America/Guayaquil': 'SA Pacific Standard Time', + 'America/Guyana': 'SA Western Standard Time', + 'America/Halifax': 'Atlantic Standard Time', + 'America/Havana': 'Eastern Standard Time', + 'America/Hermosillo': 'US Mountain Standard Time', + 'America/Indiana/Knox': 'Central Standard Time', + 'America/Indiana/Marengo': 'US Eastern Standard Time', + 'America/Indiana/Petersburg': 'Eastern Standard Time', + 'America/Indiana/Tell_City': 'Central Standard Time', + 'America/Indiana/Vevay': 'US Eastern Standard Time', + 'America/Indiana/Vincennes': 'Eastern Standard Time', + 'America/Indiana/Winamac': 'Eastern Standard Time', + 'America/Indianapolis': 'US Eastern Standard Time', + 'America/Inuvik': 'Mountain Standard Time', + 'America/Iqaluit': 'Eastern Standard Time', + 'America/Jamaica': 'SA Pacific Standard Time', + 'America/Jujuy': 'Argentina Standard Time', + 'America/Juneau': 'Alaskan Standard Time', + 'America/Kentucky/Monticello': 'Eastern Standard Time', + 'America/Kralendijk': 'SA Western Standard Time', + 'America/La_Paz': 'SA Western Standard Time', + 'America/Lima': 'SA Pacific Standard Time', + 'America/Los_Angeles': 'Pacific Standard Time', + 'America/Louisville': 'Eastern Standard Time', + 'America/Lower_Princes': 'SA Western Standard Time', + 'America/Maceio': 'SA Eastern Standard Time', + 'America/Managua': 'Central America Standard Time', + 'America/Manaus': 'SA Western Standard Time', + 'America/Marigot': 'SA Western Standard Time', + 'America/Martinique': 'SA Western Standard Time', + 'America/Matamoros': 'Central Standard Time', + 'America/Mazatlan': 'Mountain Standard Time (Mexico)', + 'America/Mendoza': 'Argentina Standard Time', + 'America/Menominee': 'Central Standard Time', + 'America/Merida': 'Central Standard Time (Mexico)', + 'America/Mexico_City': 'Central Standard Time (Mexico)', + 'America/Moncton': 'Atlantic Standard Time', + 'America/Monterrey': 'Central Standard Time (Mexico)', + 'America/Montevideo': 'Montevideo Standard Time', + 'America/Montreal': 'Eastern Standard Time', + 'America/Montserrat': 'SA Western Standard Time', + 'America/Nassau': 'Eastern Standard Time', + 'America/New_York': 'Eastern Standard Time', + 'America/Nipigon': 'Eastern Standard Time', + 'America/Nome': 'Alaskan Standard Time', + 'America/Noronha': 'UTC-02', + 'America/North_Dakota/Beulah': 'Central Standard Time', + 'America/North_Dakota/Center': 'Central Standard Time', + 'America/North_Dakota/New_Salem': 'Central Standard Time', + 'America/Ojinaga': 'Mountain Standard Time', + 'America/Panama': 'SA Pacific Standard Time', + 'America/Pangnirtung': 'Eastern Standard Time', + 'America/Paramaribo': 'SA Eastern Standard Time', + 'America/Phoenix': 'US Mountain Standard Time', + 'America/Port-au-Prince': 'Eastern Standard Time', + 'America/Port_of_Spain': 'SA Western Standard Time', + 'America/Porto_Velho': 'SA Western Standard Time', + 'America/Puerto_Rico': 'SA Western Standard Time', + 'America/Rainy_River': 'Central Standard Time', + 'America/Rankin_Inlet': 'Central Standard Time', + 'America/Recife': 'SA Eastern Standard Time', + 'America/Regina': 'Canada Central Standard Time', + 'America/Resolute': 'Central Standard Time', + 'America/Rio_Branco': 'SA Pacific Standard Time', + 'America/Santa_Isabel': 'Pacific Standard Time (Mexico)', + 'America/Santarem': 'SA Eastern Standard Time', + 'America/Santiago': 'Pacific SA Standard Time', + 'America/Santo_Domingo': 'SA Western Standard Time', + 'America/Sao_Paulo': 'E. South America Standard Time', + 'America/Scoresbysund': 'Azores Standard Time', + 'America/Shiprock': 'Mountain Standard Time', + 'America/Sitka': 'Alaskan Standard Time', + 'America/St_Barthelemy': 'SA Western Standard Time', + 'America/St_Johns': 'Newfoundland Standard Time', + 'America/St_Kitts': 'SA Western Standard Time', + 'America/St_Lucia': 'SA Western Standard Time', + 'America/St_Thomas': 'SA Western Standard Time', + 'America/St_Vincent': 'SA Western Standard Time', + 'America/Swift_Current': 'Canada Central Standard Time', + 'America/Tegucigalpa': 'Central America Standard Time', + 'America/Thule': 'Atlantic Standard Time', + 'America/Thunder_Bay': 'Eastern Standard Time', + 'America/Tijuana': 'Pacific Standard Time', + 'America/Toronto': 'Eastern Standard Time', + 'America/Tortola': 'SA Western Standard Time', + 'America/Vancouver': 'Pacific Standard Time', + 'America/Whitehorse': 'Pacific Standard Time', + 'America/Winnipeg': 'Central Standard Time', + 'America/Yakutat': 'Alaskan Standard Time', + 'America/Yellowknife': 'Mountain Standard Time', + 'Antarctica/Casey': 'W. Australia Standard Time', + 'Antarctica/Davis': 'SE Asia Standard Time', + 'Antarctica/DumontDUrville': 'West Pacific Standard Time', + 'Antarctica/Macquarie': 'Central Pacific Standard Time', + 'Antarctica/Mawson': 'West Asia Standard Time', + 'Antarctica/McMurdo': 'New Zealand Standard Time', + 'Antarctica/Palmer': 'Pacific SA Standard Time', + 'Antarctica/Rothera': 'SA Eastern Standard Time', + 'Antarctica/South_Pole': 'New Zealand Standard Time', + 'Antarctica/Syowa': 'E. Africa Standard Time', + 'Antarctica/Vostok': 'Central Asia Standard Time', + 'Arctic/Longyearbyen': 'W. Europe Standard Time', + 'Asia/Aden': 'Arab Standard Time', + 'Asia/Almaty': 'Central Asia Standard Time', + 'Asia/Amman': 'Jordan Standard Time', + 'Asia/Anadyr': 'Magadan Standard Time', + 'Asia/Aqtau': 'West Asia Standard Time', + 'Asia/Aqtobe': 'West Asia Standard Time', + 'Asia/Ashgabat': 'West Asia Standard Time', + 'Asia/Baghdad': 'Arabic Standard Time', + 'Asia/Bahrain': 'Arab Standard Time', + 'Asia/Baku': 'Azerbaijan Standard Time', + 'Asia/Bangkok': 'SE Asia Standard Time', + 'Asia/Beirut': 'Middle East Standard Time', + 'Asia/Bishkek': 'Central Asia Standard Time', + 'Asia/Brunei': 'Singapore Standard Time', + 'Asia/Calcutta': 'India Standard Time', + 'Asia/Choibalsan': 'Ulaanbaatar Standard Time', + 'Asia/Chongqing': 'China Standard Time', + 'Asia/Colombo': 'Sri Lanka Standard Time', + 'Asia/Damascus': 'Syria Standard Time', + 'Asia/Dhaka': 'Bangladesh Standard Time', + 'Asia/Dili': 'Tokyo Standard Time', + 'Asia/Dubai': 'Arabian Standard Time', + 'Asia/Dushanbe': 'West Asia Standard Time', + 'Asia/Harbin': 'China Standard Time', + 'Asia/Hong_Kong': 'China Standard Time', + 'Asia/Hovd': 'SE Asia Standard Time', + 'Asia/Irkutsk': 'North Asia East Standard Time', + 'Asia/Jakarta': 'SE Asia Standard Time', + 'Asia/Jayapura': 'Tokyo Standard Time', + 'Asia/Jerusalem': 'Israel Standard Time', + 'Asia/Kabul': 'Afghanistan Standard Time', + 'Asia/Kamchatka': 'Magadan Standard Time', + 'Asia/Karachi': 'Pakistan Standard Time', + 'Asia/Kashgar': 'China Standard Time', + 'Asia/Katmandu': 'Nepal Standard Time', + 'Asia/Khandyga': 'Yakutsk Standard Time', + 'Asia/Krasnoyarsk': 'North Asia Standard Time', + 'Asia/Kuala_Lumpur': 'Singapore Standard Time', + 'Asia/Kuching': 'Singapore Standard Time', + 'Asia/Kuwait': 'Arab Standard Time', + 'Asia/Macau': 'China Standard Time', + 'Asia/Magadan': 'Magadan Standard Time', + 'Asia/Makassar': 'Singapore Standard Time', + 'Asia/Manila': 'Singapore Standard Time', + 'Asia/Muscat': 'Arabian Standard Time', + 'Asia/Nicosia': 'E. Europe Standard Time', + 'Asia/Novokuznetsk': 'N. Central Asia Standard Time', + 'Asia/Novosibirsk': 'N. Central Asia Standard Time', + 'Asia/Omsk': 'N. Central Asia Standard Time', + 'Asia/Oral': 'West Asia Standard Time', + 'Asia/Phnom_Penh': 'SE Asia Standard Time', + 'Asia/Pontianak': 'SE Asia Standard Time', + 'Asia/Pyongyang': 'Korea Standard Time', + 'Asia/Qatar': 'Arab Standard Time', + 'Asia/Qyzylorda': 'Central Asia Standard Time', + 'Asia/Rangoon': 'Myanmar Standard Time', + 'Asia/Riyadh': 'Arab Standard Time', + 'Asia/Saigon': 'SE Asia Standard Time', + 'Asia/Sakhalin': 'Vladivostok Standard Time', + 'Asia/Samarkand': 'West Asia Standard Time', + 'Asia/Seoul': 'Korea Standard Time', + 'Asia/Shanghai': 'China Standard Time', + 'Asia/Singapore': 'Singapore Standard Time', + 'Asia/Taipei': 'Taipei Standard Time', + 'Asia/Tashkent': 'West Asia Standard Time', + 'Asia/Tbilisi': 'Georgian Standard Time', + 'Asia/Tehran': 'Iran Standard Time', + 'Asia/Thimphu': 'Bangladesh Standard Time', + 'Asia/Tokyo': 'Tokyo Standard Time', + 'Asia/Ulaanbaatar': 'Ulaanbaatar Standard Time', + 'Asia/Urumqi': 'China Standard Time', + 'Asia/Ust-Nera': 'Vladivostok Standard Time', + 'Asia/Vientiane': 'SE Asia Standard Time', + 'Asia/Vladivostok': 'Vladivostok Standard Time', + 'Asia/Yakutsk': 'Yakutsk Standard Time', + 'Asia/Yekaterinburg': 'Ekaterinburg Standard Time', + 'Asia/Yerevan': 'Caucasus Standard Time', + 'Atlantic/Azores': 'Azores Standard Time', + 'Atlantic/Bermuda': 'Atlantic Standard Time', + 'Atlantic/Canary': 'GMT Standard Time', + 'Atlantic/Cape_Verde': 'Cape Verde Standard Time', + 'Atlantic/Faeroe': 'GMT Standard Time', + 'Atlantic/Madeira': 'GMT Standard Time', + 'Atlantic/Reykjavik': 'Greenwich Standard Time', + 'Atlantic/South_Georgia': 'UTC-02', + 'Atlantic/St_Helena': 'Greenwich Standard Time', + 'Atlantic/Stanley': 'SA Eastern Standard Time', + 'Australia/Adelaide': 'Cen. Australia Standard Time', + 'Australia/Brisbane': 'E. Australia Standard Time', + 'Australia/Broken_Hill': 'Cen. Australia Standard Time', + 'Australia/Currie': 'Tasmania Standard Time', + 'Australia/Darwin': 'AUS Central Standard Time', + 'Australia/Hobart': 'Tasmania Standard Time', + 'Australia/Lindeman': 'E. Australia Standard Time', + 'Australia/Melbourne': 'AUS Eastern Standard Time', + 'Australia/Perth': 'W. Australia Standard Time', + 'Australia/Sydney': 'AUS Eastern Standard Time', + 'CST6CDT': 'Central Standard Time', + 'EST5EDT': 'Eastern Standard Time', + 'Etc/GMT': 'UTC', + 'Etc/GMT+1': 'Cape Verde Standard Time', + 'Etc/GMT+10': 'Hawaiian Standard Time', + 'Etc/GMT+11': 'UTC-11', + 'Etc/GMT+12': 'Dateline Standard Time', + 'Etc/GMT+2': 'UTC-02', + 'Etc/GMT+3': 'SA Eastern Standard Time', + 'Etc/GMT+4': 'SA Western Standard Time', + 'Etc/GMT+5': 'SA Pacific Standard Time', + 'Etc/GMT+6': 'Central America Standard Time', + 'Etc/GMT+7': 'US Mountain Standard Time', + 'Etc/GMT-1': 'W. Central Africa Standard Time', + 'Etc/GMT-10': 'West Pacific Standard Time', + 'Etc/GMT-11': 'Central Pacific Standard Time', + 'Etc/GMT-12': 'UTC+12', + 'Etc/GMT-13': 'Tonga Standard Time', + 'Etc/GMT-2': 'South Africa Standard Time', + 'Etc/GMT-3': 'E. Africa Standard Time', + 'Etc/GMT-4': 'Arabian Standard Time', + 'Etc/GMT-5': 'West Asia Standard Time', + 'Etc/GMT-6': 'Central Asia Standard Time', + 'Etc/GMT-7': 'SE Asia Standard Time', + 'Etc/GMT-8': 'Singapore Standard Time', + 'Etc/GMT-9': 'Tokyo Standard Time', + 'Etc/UTC': 'UTC', + 'Europe/Amsterdam': 'W. Europe Standard Time', + 'Europe/Andorra': 'W. Europe Standard Time', + 'Europe/Athens': 'GTB Standard Time', + 'Europe/Belgrade': 'Central Europe Standard Time', + 'Europe/Berlin': 'W. Europe Standard Time', + 'Europe/Bratislava': 'Central Europe Standard Time', + 'Europe/Brussels': 'Romance Standard Time', + 'Europe/Bucharest': 'GTB Standard Time', + 'Europe/Budapest': 'Central Europe Standard Time', + 'Europe/Busingen': 'W. Europe Standard Time', + 'Europe/Chisinau': 'GTB Standard Time', + 'Europe/Copenhagen': 'Romance Standard Time', + 'Europe/Dublin': 'GMT Standard Time', + 'Europe/Gibraltar': 'W. Europe Standard Time', + 'Europe/Guernsey': 'GMT Standard Time', + 'Europe/Helsinki': 'FLE Standard Time', + 'Europe/Isle_of_Man': 'GMT Standard Time', + 'Europe/Istanbul': 'Turkey Standard Time', + 'Europe/Jersey': 'GMT Standard Time', + 'Europe/Kaliningrad': 'Kaliningrad Standard Time', + 'Europe/Kiev': 'FLE Standard Time', + 'Europe/Lisbon': 'GMT Standard Time', + 'Europe/Ljubljana': 'Central Europe Standard Time', + 'Europe/London': 'GMT Standard Time', + 'Europe/Luxembourg': 'W. Europe Standard Time', + 'Europe/Madrid': 'Romance Standard Time', + 'Europe/Malta': 'W. Europe Standard Time', + 'Europe/Mariehamn': 'FLE Standard Time', + 'Europe/Minsk': 'Kaliningrad Standard Time', + 'Europe/Monaco': 'W. Europe Standard Time', + 'Europe/Moscow': 'Russian Standard Time', + 'Europe/Oslo': 'W. Europe Standard Time', + 'Europe/Paris': 'Romance Standard Time', + 'Europe/Podgorica': 'Central Europe Standard Time', + 'Europe/Prague': 'Central Europe Standard Time', + 'Europe/Riga': 'FLE Standard Time', + 'Europe/Rome': 'W. Europe Standard Time', + 'Europe/Samara': 'Russian Standard Time', + 'Europe/San_Marino': 'W. Europe Standard Time', + 'Europe/Sarajevo': 'Central European Standard Time', + 'Europe/Simferopol': 'FLE Standard Time', + 'Europe/Skopje': 'Central European Standard Time', + 'Europe/Sofia': 'FLE Standard Time', + 'Europe/Stockholm': 'W. Europe Standard Time', + 'Europe/Tallinn': 'FLE Standard Time', + 'Europe/Tirane': 'Central Europe Standard Time', + 'Europe/Uzhgorod': 'FLE Standard Time', + 'Europe/Vaduz': 'W. Europe Standard Time', + 'Europe/Vatican': 'W. Europe Standard Time', + 'Europe/Vienna': 'W. Europe Standard Time', + 'Europe/Vilnius': 'FLE Standard Time', + 'Europe/Volgograd': 'Russian Standard Time', + 'Europe/Warsaw': 'Central European Standard Time', + 'Europe/Zagreb': 'Central European Standard Time', + 'Europe/Zaporozhye': 'FLE Standard Time', + 'Europe/Zurich': 'W. Europe Standard Time', + 'Indian/Antananarivo': 'E. Africa Standard Time', + 'Indian/Chagos': 'Central Asia Standard Time', + 'Indian/Christmas': 'SE Asia Standard Time', + 'Indian/Cocos': 'Myanmar Standard Time', + 'Indian/Comoro': 'E. Africa Standard Time', + 'Indian/Kerguelen': 'West Asia Standard Time', + 'Indian/Mahe': 'Mauritius Standard Time', + 'Indian/Maldives': 'West Asia Standard Time', + 'Indian/Mauritius': 'Mauritius Standard Time', + 'Indian/Mayotte': 'E. Africa Standard Time', + 'Indian/Reunion': 'Mauritius Standard Time', + 'MST7MDT': 'Mountain Standard Time', + 'PST8PDT': 'Pacific Standard Time', + 'Pacific/Apia': 'Samoa Standard Time', + 'Pacific/Auckland': 'New Zealand Standard Time', + 'Pacific/Efate': 'Central Pacific Standard Time', + 'Pacific/Enderbury': 'Tonga Standard Time', + 'Pacific/Fakaofo': 'Tonga Standard Time', + 'Pacific/Fiji': 'Fiji Standard Time', + 'Pacific/Funafuti': 'UTC+12', + 'Pacific/Galapagos': 'Central America Standard Time', + 'Pacific/Guadalcanal': 'Central Pacific Standard Time', + 'Pacific/Guam': 'West Pacific Standard Time', + 'Pacific/Honolulu': 'Hawaiian Standard Time', + 'Pacific/Johnston': 'Hawaiian Standard Time', + 'Pacific/Kosrae': 'Central Pacific Standard Time', + 'Pacific/Kwajalein': 'UTC+12', + 'Pacific/Majuro': 'UTC+12', + 'Pacific/Midway': 'UTC-11', + 'Pacific/Nauru': 'UTC+12', + 'Pacific/Niue': 'UTC-11', + 'Pacific/Noumea': 'Central Pacific Standard Time', + 'Pacific/Pago_Pago': 'UTC-11', + 'Pacific/Palau': 'Tokyo Standard Time', + 'Pacific/Ponape': 'Central Pacific Standard Time', + 'Pacific/Port_Moresby': 'West Pacific Standard Time', + 'Pacific/Rarotonga': 'Hawaiian Standard Time', + 'Pacific/Saipan': 'West Pacific Standard Time', + 'Pacific/Tahiti': 'Hawaiian Standard Time', + 'Pacific/Tarawa': 'UTC+12', + 'Pacific/Tongatapu': 'Tonga Standard Time', + 'Pacific/Truk': 'West Pacific Standard Time', + 'Pacific/Wake': 'UTC+12', + 'Pacific/Wallis': 'UTC+12' +} diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 516df92..4109f2d 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -25,12 +25,15 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import absolute_import +import sys +import os __all__ = [ 'Command', 'LateTask', 'PageCompiler', 'RestExtension', + 'MarkdownExtension', 'Task', 'TaskMultiplier', 'TemplateSystem', @@ -42,12 +45,6 @@ from doit.cmd_base import Command as DoitCommand from .utils import LOGGER, first_line -try: - # ordereddict does not exist in py2.6 - from collections import OrderedDict -except ImportError: - OrderedDict = None # NOQA - class BasePlugin(IPlugin): """Base plugin class.""" @@ -55,6 +52,28 @@ class BasePlugin(IPlugin): def set_site(self, site): """Sets site, which is a Nikola instance.""" self.site = site + self.inject_templates() + + def inject_templates(self): + """If this plugin contains a 'templates' folder, + then templates/mako or templates/jinja will be inserted very early in + the theme chain.""" + + try: + # Sorry, found no other way to get this + mod_path = sys.modules[self.__class__.__module__].__file__ + mod_dir = os.path.dirname(mod_path) + tmpl_dir = os.path.join( + mod_dir, 'templates', self.site.template_system.name + ) + if os.path.isdir(tmpl_dir): + # Inject tmpl_dir low in the theme chain + self.site.template_system.inject_directory(tmpl_dir) + except AttributeError: + # In some cases, __builtin__ becomes the module of a plugin. + # We couldn’t reproduce that, and really find the reason for this, + # so let’s just ignore it and be done with it. + pass class Command(BasePlugin, DoitCommand): @@ -135,6 +154,8 @@ class BaseTask(BasePlugin): class Task(BaseTask): """Plugins of this type are task generators.""" + name = "dummy_task" + class LateTask(BaseTask): """Plugins of this type are executed after all plugins of type Task.""" @@ -145,16 +166,12 @@ class LateTask(BaseTask): class TemplateSystem(BasePlugin): """Plugins of this type wrap templating systems.""" - name = "dummy templates" + name = "dummy_templates" def set_directories(self, directories, cache_folder): """Sets the list of folders where templates are located and cache.""" raise NotImplementedError() - def set_site(self, site): - """Sets the site.""" - self.site = site - def template_deps(self, template_name): """Returns filenames which are dependencies for a template.""" raise NotImplementedError() @@ -168,8 +185,12 @@ class TemplateSystem(BasePlugin): raise NotImplementedError() def render_template_to_string(self, template, context): - """ Renders template to a string using context. """ + """Renders template to a string using context. """ + raise NotImplementedError() + def inject_directory(self, directory): + """Injects the directory with the lowest priority in the + template search mechanism.""" raise NotImplementedError() @@ -189,27 +210,24 @@ class PageCompiler(BasePlugin): name = "dummy compiler" demote_headers = False - if OrderedDict is not None: - default_metadata = OrderedDict() - else: - # Graceful fallback. We could use a backport, but it is - # not going to change anything, other than a bit uglier - # and nonsensical layout. Not enough to care. - default_metadata = {} - - default_metadata['title'] = '' - default_metadata['slug'] = '' - default_metadata['date'] = '' - default_metadata['tags'] = '' - default_metadata['link'] = '' - default_metadata['description'] = '' - default_metadata['type'] = 'text' + supports_onefile = True + default_metadata = {} + + default_metadata = { + 'title': '', + 'slug': '', + 'date': '', + 'tags': '', + 'link': '', + 'description': '', + 'type': 'text', + } def compile_html(self, source, dest, is_two_file=False): """Compile the source, save it on dest.""" raise NotImplementedError() - def create_post(self, path, onefile=False, **kw): + def create_post(self, path, content=None, onefile=False, is_page=False, **kw): """Create post file with optional metadata.""" raise NotImplementedError() @@ -222,6 +240,10 @@ class RestExtension(BasePlugin): name = "dummy_rest_extension" +class MarkdownExtension(BasePlugin): + name = "dummy_markdown_extension" + + class SignalHandler(BasePlugin): name = "dummy_signal_handler" diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py index 27c0eb4..7b23f9c 100644 --- a/nikola/plugins/basic_import.py +++ b/nikola/plugins/basic_import.py @@ -29,6 +29,7 @@ import codecs import csv import datetime import os +from pkg_resources import resource_filename try: from urlparse import urlparse @@ -89,13 +90,13 @@ class ImportMixin(object): def generate_base_site(self): if not os.path.exists(self.output_folder): - os.system('nikola init ' + self.output_folder) + os.system('nikola init -q ' + 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') + filename = resource_filename('nikola', '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 , diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py index d707d53..c46e0a3 100644 --- a/nikola/plugins/command/auto.py +++ b/nikola/plugins/command/auto.py @@ -73,11 +73,11 @@ class CommandAuto(Command): server.watch('conf.py', 'nikola build') server.watch('themes/', 'nikola build') server.watch('templates/', 'nikola build') - server.watch(self.site.config['GALLERY_PATH']) + server.watch(self.site.config['GALLERY_PATH'], 'nikola build') for item in self.site.config['post_pages']: server.watch(os.path.dirname(item[0]), 'nikola build') for item in self.site.config['FILES_FOLDERS']: - server.watch(os.path.dirname(item), 'nikola build') + server.watch(item, 'nikola build') out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py index 82c47d2..871a5ce 100644 --- a/nikola/plugins/command/bootswatch_theme.py +++ b/nikola/plugins/command/bootswatch_theme.py @@ -92,7 +92,10 @@ class CommandBootswatchTheme(Command): LOGGER.info("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', version, swatch, fname)) + url = 'http://bootswatch.com' + if version: + url += '/' + version + url = '/'.join((url, swatch, fname)) LOGGER.info("Downloading: " + url) data = requests.get(url).text with open(os.path.join('themes', name, 'assets', 'css', fname), diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py index 26db321..76571a0 100644 --- a/nikola/plugins/command/check.py +++ b/nikola/plugins/command/check.py @@ -51,7 +51,7 @@ def real_scan_files(site): 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 root, dirs, files in os.walk(output_folder, followlinks=True): for src_name in files: fname = os.path.join(root, src_name) real_fnames.add(fname) @@ -139,6 +139,8 @@ class CommandCheck(Command): rv = False self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']] base_url = urlparse(self.site.config['BASE_URL']) + self.existing_targets.add(self.site.config['SITE_URL']) + self.existing_targets.add(self.site.config['BASE_URL']) url_type = self.site.config['URL_TYPE'] try: filename = task.split(":")[-1] @@ -166,11 +168,15 @@ class CommandCheck(Command): elif url_type in ('full_path', 'absolute'): target_filename = os.path.abspath( os.path.join(os.path.dirname(filename), parsed.path)) - if parsed.path.endswith('/'): # abspath removes trailing slashes + if parsed.path in ['', '/']: + target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.config['INDEX_FILE']) + elif parsed.path.endswith('/'): # abspath removes trailing slashes target_filename += '/{0}'.format(self.site.config['INDEX_FILE']) if target_filename.startswith(base_url.path): target_filename = target_filename[len(base_url.path):] target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], target_filename) + if parsed.path in ['', '/']: + target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.config['INDEX_FILE']) if any(re.match(x, target_filename) for x in self.whitelist): continue @@ -233,7 +239,7 @@ class CommandCheck(Command): return failure def clean_files(self): - only_on_output, _ = self.real_scan_files() + only_on_output, _ = real_scan_files(self.site) for f in only_on_output: os.unlink(f) return True diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py index b0a8958..9dfc975 100644 --- a/nikola/plugins/command/console.py +++ b/nikola/plugins/command/console.py @@ -30,7 +30,7 @@ import os from nikola import __version__ from nikola.plugin_categories import Command -from nikola.utils import get_logger, STDERR_HANDLER +from nikola.utils import get_logger, STDERR_HANDLER, req_missing LOGGER = get_logger('console', STDERR_HANDLER) @@ -41,86 +41,102 @@ class CommandConsole(Command): shells = ['ipython', 'bpython', 'plain'] doc_purpose = "start an interactive Python console with access to your site" doc_description = """\ -Order of resolution: IPython → bpython [deprecated] → plain Python interpreter -The site engine is accessible as `SITE`, and the config as `conf`.""" - header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)" +The site engine is accessible as `site`, the config file as `conf`, and commands are available as `commands`. +If there is no console to use specified (as -b, -i, -p) it tries IPython, then falls back to bpython, and finally falls back to the plain Python console.""" + header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site = site engine, commands = nikola commands)" cmd_options = [ { + 'name': 'bpython', + 'short': 'b', + 'long': 'bpython', + 'type': bool, + 'default': False, + 'help': 'Use bpython', + }, + { + 'name': 'ipython', + 'short': 'i', + 'long': 'plain', + 'type': bool, + 'default': False, + 'help': 'Use IPython', + }, + { 'name': 'plain', 'short': 'p', 'long': 'plain', 'type': bool, 'default': False, - 'help': 'Force the plain Python console', - } + 'help': 'Use the plain Python interpreter', + }, ] - def ipython(self): + def ipython(self, willful=True): """IPython shell.""" - from nikola import Nikola try: - import conf - except ImportError: - LOGGER.error("No configuration found, cannot run the console.") - else: import IPython - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() + except ImportError as e: + if willful: + req_missing(['IPython'], 'use the IPython console') + raise e # That’s how _execute knows whether to try something else. + else: + site = self.context['site'] # NOQA + conf = self.context['conf'] # NOQA + commands = self.context['commands'] # NOQA IPython.embed(header=self.header.format('IPython')) - def bpython(self): + def bpython(self, willful=True): """bpython shell.""" - from nikola import Nikola try: - import conf - except ImportError: - 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=self.header.format( - 'bpython (Slightly Deprecated)'), locals_=gl) + except ImportError as e: + if willful: + req_missing(['bpython'], 'use the bpython console') + raise e # That’s how _execute knows whether to try something else. + else: + bpython.embed(banner=self.header.format('bpython'), locals_=self.context) - def plain(self): + def plain(self, willful=True): """Plain Python shell.""" - from nikola import Nikola + import code try: - import conf - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() - gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} + import readline except ImportError: - LOGGER.error("No configuration found, cannot run the console.") + pass else: - import code + import rlcompleter + readline.set_completer(rlcompleter.Completer(self.context).complete) + readline.parse_and_bind("tab:complete") + + pythonrc = os.environ.get("PYTHONSTARTUP") + if pythonrc and os.path.isfile(pythonrc): try: - import readline - except ImportError: + execfile(pythonrc) # NOQA + except NameError: pass - else: - import rlcompleter - readline.set_completer(rlcompleter.Completer(gl).complete) - readline.parse_and_bind("tab:complete") - pythonrc = os.environ.get("PYTHONSTARTUP") - if pythonrc and os.path.isfile(pythonrc): - try: - execfile(pythonrc) # NOQA - except NameError: - pass - - code.interact(local=gl, banner=self.header.format('Python')) + code.interact(local=self.context, banner=self.header.format('Python')) def _execute(self, options, args): """Start the console.""" - if options['plain']: - self.plain() + self.site.scan_posts() + # Create nice object with all commands: + + self.context = { + 'conf': self.site.config, + 'site': self.site, + 'commands': self.site.commands, + } + if options['bpython']: + self.bpython(True) + elif options['ipython']: + self.ipython(True) + elif options['plain']: + self.plain(True) else: for shell in self.shells: try: - return getattr(self, shell)() + return getattr(self, shell)(False) except ImportError: pass raise ImportError diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py index bd1c15f..1bec1d3 100644 --- a/nikola/plugins/command/deploy.py +++ b/nikola/plugins/command/deploy.py @@ -31,7 +31,6 @@ import os import sys import subprocess import time -import pytz from blinker import signal @@ -62,10 +61,10 @@ class CommandDeploy(Command): deploy_drafts = self.site.config.get('DEPLOY_DRAFTS', True) deploy_future = self.site.config.get('DEPLOY_FUTURE', False) + undeployed_posts = [] 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 \ @@ -114,9 +113,6 @@ class CommandDeploy(Command): """ - if undeployed is None: - undeployed = [] - event = { 'last_deploy': last_deploy, 'new_deploy': new_deploy, @@ -124,11 +120,9 @@ class CommandDeploy(Command): 'undeployed': undeployed } - tzinfo = pytz.timezone(self.site.config['TIMEZONE']) - deployed = [ entry for entry in self.site.timeline - if entry.date > (last_deploy.replace(tzinfo=tzinfo) if tzinfo else last_deploy) and entry not in undeployed + if entry.date > last_deploy.replace(tzinfo=self.site.tzinfo) and entry not in undeployed ] event['deployed'] = deployed diff --git a/nikola/plugins/command/github_deploy.plugin b/nikola/plugins/command/github_deploy.plugin new file mode 100644 index 0000000..4cbc422 --- /dev/null +++ b/nikola/plugins/command/github_deploy.plugin @@ -0,0 +1,9 @@ +[Core] +Name = github_deploy +Module = github_deploy + +[Documentation] +Author = Puneeth Chaganti +Version = 0.1 +Website = http://getnikola.com +Description = Deploy the site to GitHub pages. diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py new file mode 100644 index 0000000..d4dd8c5 --- /dev/null +++ b/nikola/plugins/command/github_deploy.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2014 Puneeth Chaganti 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 shutil +import subprocess +import sys +from textwrap import dedent + +from nikola.plugin_categories import Command +from nikola.plugins.command.check import real_scan_files +from nikola.utils import ask_yesno, get_logger +from nikola.__main__ import main +from nikola import __version__ + + +def uni_check_output(*args, **kwargs): + o = subprocess.check_output(*args, **kwargs) + return o.decode('utf-8') + + +class CommandGitHubDeploy(Command): + """ Deploy site to GitHub pages. """ + name = 'github_deploy' + + doc_usage = '' + doc_purpose = 'deploy the site to GitHub pages' + doc_description = dedent( + """\ + This command can be used to deploy your site to GitHub pages. + It performs the following actions: + + 1. Ensure that your site is a git repository, and git is on the PATH. + 2. Ensure that the output directory is not committed on the + source branch. + 3. Check for changes, and prompt the user to continue, if required. + 4. Build the site + 5. Clean any files that are "unknown" to Nikola. + 6. Create a deploy branch, if one doesn't exist. + 7. Commit the output to this branch. (NOTE: Any untracked source + files, may get committed at this stage, on the wrong branch!) + 8. Push and deploy! + + NOTE: This command needs your site to be a git repository, with a + master branch (or a different branch, configured using + GITHUB_SOURCE_BRANCH if you are pushing to user.github + .io/organization.github.io pages) containing the sources of your + site. You also, obviously, need to have `git` on your PATH, + and should be able to push to the repository specified as the remote + (origin, by default). + """ + ) + + logger = None + + _deploy_branch = '' + _source_branch = '' + _remote_name = '' + + def _execute(self, command, args): + + self.logger = get_logger( + CommandGitHubDeploy.name, self.site.loghandlers + ) + self._source_branch = self.site.config.get( + 'GITHUB_SOURCE_BRANCH', 'master' + ) + self._deploy_branch = self.site.config.get( + 'GITHUB_DEPLOY_BRANCH', 'gh-pages' + ) + self._remote_name = self.site.config.get( + 'GITHUB_REMOTE_NAME', 'origin' + ) + + self._ensure_git_repo() + + self._exit_if_output_committed() + + if not self._prompt_continue(): + return + + build = main(['build']) + if build != 0: + self.logger.error('Build failed, not deploying to GitHub') + sys.exit(build) + + only_on_output, _ = real_scan_files(self.site) + for f in only_on_output: + os.unlink(f) + + self._checkout_deploy_branch() + + self._copy_output() + + self._commit_and_push() + + return + + def _commit_and_push(self): + """ Commit all the files and push. """ + + deploy = self._deploy_branch + source = self._source_branch + remote = self._remote_name + + source_commit = uni_check_output(['git', 'rev-parse', source]) + commit_message = ( + 'Nikola auto commit.\n\n' + 'Source commit: %s' + 'Nikola version: %s' % (source_commit, __version__) + ) + + commands = [ + ['git', 'add', '-A'], + ['git', 'commit', '-m', commit_message], + ['git', 'push', '-f', remote, '%s:%s' % (deploy, deploy)], + ['git', 'checkout', source], + ] + + for command in commands: + self.logger.info("==> {0}".format(command)) + try: + subprocess.check_call(command) + except subprocess.CalledProcessError as e: + self.logger.error( + 'Failed GitHub deployment — command {0} ' + 'returned {1}'.format(e.cmd, e.returncode) + ) + sys.exit(e.returncode) + + def _copy_output(self): + """ Copy all output to the top level directory. """ + output_folder = self.site.config['OUTPUT_FOLDER'] + for each in os.listdir(output_folder): + if os.path.exists(each): + if os.path.isdir(each): + shutil.rmtree(each) + + else: + os.unlink(each) + + shutil.move(os.path.join(output_folder, each), '.') + + def _checkout_deploy_branch(self): + """ Check out the deploy branch + + Creates an orphan branch if not present. + + """ + + deploy = self._deploy_branch + + try: + subprocess.check_call( + [ + 'git', 'show-ref', '--verify', '--quiet', + 'refs/heads/%s' % deploy + ] + ) + except subprocess.CalledProcessError: + self._create_orphan_deploy_branch() + else: + subprocess.check_call(['git', 'checkout', deploy]) + + def _create_orphan_deploy_branch(self): + """ Create an orphan deploy branch """ + + result = subprocess.check_call( + ['git', 'checkout', '--orphan', self._deploy_branch] + ) + if result != 0: + self.logger.error('Failed to create a deploy branch') + sys.exit(1) + + result = subprocess.check_call(['git', 'rm', '-rf', '.']) + if result != 0: + self.logger.error('Failed to create a deploy branch') + sys.exit(1) + + with open('.gitignore', 'w') as f: + f.write('%s\n' % self.site.config['OUTPUT_FOLDER']) + f.write('%s\n' % self.site.config['CACHE_FOLDER']) + f.write('*.pyc\n') + f.write('*.db\n') + + subprocess.check_call(['git', 'add', '.gitignore']) + subprocess.check_call(['git', 'commit', '-m', 'Add .gitignore']) + + def _ensure_git_repo(self): + """ Ensure that the site is a git-repo. + + Also make sure that a remote with the specified name exists. + + """ + + try: + remotes = uni_check_output(['git', 'remote']) + except subprocess.CalledProcessError as e: + self.logger.notice('github_deploy needs a git repository!') + sys.exit(e.returncode) + except OSError as e: + import errno + self.logger.error('Running git failed with {0}'.format(e)) + if e.errno == errno.ENOENT: + self.logger.notice('Is git on the PATH?') + sys.exit(1) + else: + if self._remote_name not in remotes: + self.logger.error( + 'Need a remote called "%s" configured' % self._remote_name + ) + sys.exit(1) + + def _exit_if_output_committed(self): + """ Exit if the output folder is committed on the source branch. """ + + source = self._source_branch + subprocess.check_call(['git', 'checkout', source]) + + output_folder = self.site.config['OUTPUT_FOLDER'] + output_log = uni_check_output( + ['git', 'ls-files', '--', output_folder] + ) + + if len(output_log.strip()) > 0: + self.logger.error( + 'Output folder is committed on the source branch. ' + 'Cannot proceed until it is removed.' + ) + sys.exit(1) + + def _prompt_continue(self): + """ Show uncommitted changes, and ask if user wants to continue. """ + + changes = uni_check_output(['git', 'status', '--porcelain']) + if changes.strip(): + changes = uni_check_output(['git', 'status']).strip() + message = ( + "You have the following changes:\n%s\n\n" + "Anything not committed, and unknown to Nikola may be lost, " + "or committed onto the wrong branch. Do you wish to continue?" + ) % changes + proceed = ask_yesno(message, False) + else: + proceed = True + + return proceed diff --git a/nikola/plugins/command/import_blogger.plugin b/nikola/plugins/command/import_blogger.plugin deleted file mode 100644 index 91a7cb6..0000000 --- a/nikola/plugins/command/import_blogger.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = import_blogger -Module = import_blogger - -[Documentation] -Author = Roberto Alsina -Version = 0.2 -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 deleted file mode 100644 index dd629c4..0000000 --- a/nikola/plugins/command/import_blogger.py +++ /dev/null @@ -1,228 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 -from nikola.plugins.command.init import SAMPLE_CONF, prepare_config - -LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER) - - -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 = ImportMixin.cmd_options + [ - { - 'name': 'exclude_drafts', - 'long': 'no-drafts', - 'short': 'd', - 'default': False, - 'type': bool, - 'help': "Don't import drafts", - }, - ] - - def _execute(self, options, args): - """Import a Blogger blog from an export file into a Nikola site.""" - # Parse the data - if feedparser is None: - req_missing(['feedparser'], 'import Blogger dumps') - return - - if not args: - print(self.help()) - return - - options['filename'] = args[0] - self.blogger_export_file = options['filename'] - self.output_folder = options['output_folder'] - self.import_into_existing_site = False - self.exclude_drafts = options['exclude_drafts'] - self.url_map = {} - channel = self.get_channel_from_file(self.blogger_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_urlmap_csv( - os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - - conf_out_path = self.get_configuration_output_path() - # if it tracebacks here, look a comment in - # basic_import.Import_Mixin.generate_base_site - conf_template_render = conf_template.render(**prepare_config(self.context)) - self.write_configuration(conf_out_path, conf_template_render) - - @classmethod - def get_channel_from_file(cls, filename): - if not os.path.isfile(filename): - raise Exception("Missing file: %s" % filename) - return feedparser.parse(filename) - - @staticmethod - def populate_context(channel): - context = SAMPLE_CONF.copy() - 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 - context['BLOG_EMAIL'] = channel.feed.author_detail.email - context['BLOG_AUTHOR'] = channel.feed.author_detail.name - 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') - } - ''' - - return context - - 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)) - - for candidate in item.content: - if candidate.type == 'text/html': - content = candidate.value - break - # FIXME: handle attachments - - tags = [] - for tag in item.tags: - if tag.scheme == 'http://www.blogger.com/atom/ns#': - 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)) - - def process_item(self, item): - post_type = item.tags[0].term - - if post_type == 'http://schemas.google.com/blogger/2008/kind#post': - self.import_item(item, 'posts') - elif post_type == 'http://schemas.google.com/blogger/2008/kind#page': - self.import_item(item, 'stories') - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#settings'): - # Ignore settings - pass - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#template'): - # Ignore template - pass - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#comment'): - # FIXME: not importing comments. Does blogger support "pages"? - pass - else: - LOGGER.warn("Unknown post_type:", post_type) - - def import_posts(self, channel): - for item in channel.entries: - self.process_item(item) diff --git a/nikola/plugins/command/import_feed.plugin b/nikola/plugins/command/import_feed.plugin deleted file mode 100644 index 26e570a..0000000 --- a/nikola/plugins/command/import_feed.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index ee59277..0000000 --- a/nikola/plugins/command/import_feed.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 -from nikola.plugins.command.init import SAMPLE_CONF, prepare_config - -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(**prepare_config(self.context))) - - @classmethod - def get_channel_from_file(cls, filename): - return feedparser.parse(filename) - - @staticmethod - def populate_context(channel): - context = SAMPLE_CONF.copy() - 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['POSTS'] = '''( - ("posts/*.html", "posts", "post.tmpl"), - )''' - context['PAGES'] = '''( - ("stories/*.html", "stories", "story.tmpl"), - )''' - 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.py b/nikola/plugins/command/import_wordpress.py index b567c77..8ddc8c7 100644 --- a/nikola/plugins/command/import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -51,7 +51,7 @@ from nikola import utils from nikola.utils import req_missing from nikola.plugins.basic_import import ImportMixin, links from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN -from nikola.plugins.command.init import SAMPLE_CONF, prepare_config +from nikola.plugins.command.init import SAMPLE_CONF, prepare_config, format_default_translations_config LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) @@ -136,6 +136,9 @@ class CommandImportWordpress(Command, ImportMixin): self.separate_qtranslate_content = options.get('separate_qtranslate_content') self.translations_pattern = options.get('translations_pattern') + # A place holder where extra language (if detected) will be stored + self.extra_languages = set() + if not self.no_downloads: def show_info_about_mising_module(modulename): LOGGER.error( @@ -164,6 +167,8 @@ class CommandImportWordpress(Command, ImportMixin): self.import_posts(channel) + self.context['TRANSLATIONS'] = format_default_translations_config( + self.extra_languages) self.context['REDIRECTIONS'] = self.configure_redirections( self.url_map) self.write_urlmap_csv( @@ -326,7 +331,7 @@ class CommandImportWordpress(Command, ImportMixin): size_key = b'sizes' file_key = b'file' - if not size_key in metadata: + if size_key not in metadata: continue for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]: @@ -452,6 +457,7 @@ class CommandImportWordpress(Command, ImportMixin): out_content_filename \ = utils.get_translation_candidate(self.context, slug + ".wp", lang) + self.extra_languages.add(lang) meta_slug = slug else: out_meta_filename = slug + '.meta' diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py index d7eeed7..8fb15e0 100644 --- a/nikola/plugins/command/init.py +++ b/nikola/plugins/command/init.py @@ -24,19 +24,24 @@ # 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 __future__ import print_function, unicode_literals import os import shutil import codecs import json - +import textwrap +import datetime +import unidecode +import dateutil.tz from mako.template import Template +from pkg_resources import resource_filename import nikola -from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN +from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN, DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_RSS_READ_MORE_LINK, LEGAL_VALUES from nikola.plugin_categories import Command -from nikola.utils import get_logger, makedirs, STDERR_HANDLER -from nikola.winutils import fix_git_symlinked +from nikola.utils import ask, ask_yesno, get_logger, makedirs, STDERR_HANDLER, load_messages +from nikola.packages.tzlocal import get_localzone + LOGGER = get_logger('init', STDERR_HANDLER) @@ -47,39 +52,144 @@ SAMPLE_CONF = { 'BLOG_EMAIL': "joe@demo.site", 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", 'DEFAULT_LANG': "en", + 'TRANSLATIONS': """{ + DEFAULT_LANG: "", + # Example for another language: + # "es": "./es", +}""", 'THEME': 'bootstrap3', + 'TIMEZONE': 'UTC', 'COMMENT_SYSTEM': 'disqus', 'COMMENT_SYSTEM_ID': 'nikolademo', 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, + 'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK, + 'RSS_READ_MORE_LINK': DEFAULT_RSS_READ_MORE_LINK, 'POSTS': """( -("posts/*.rst", "posts", "post.tmpl"), -("posts/*.txt", "posts", "post.tmpl"), + ("posts/*.rst", "posts", "post.tmpl"), + ("posts/*.txt", "posts", "post.tmpl"), )""", 'PAGES': """( -("stories/*.rst", "stories", "story.tmpl"), -("stories/*.txt", "stories", "story.tmpl"), + ("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'), -# PHP files are rendered the usual way (i.e. with the full templates). -# The resulting files have .php extensions, making it possible to run -# them without reconfiguring your server to recognize them. -"php": ('.php',), -# 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'), + "rest": ('.rst', '.txt'), + "markdown": ('.md', '.mdown', '.markdown'), + "textile": ('.textile',), + "txt2tags": ('.t2t',), + "bbcode": ('.bb',), + "wiki": ('.wiki',), + "ipynb": ('.ipynb',), + "html": ('.html', '.htm'), + # PHP files are rendered the usual way (i.e. with the full templates). + # The resulting files have .php extensions, making it possible to run + # them without reconfiguring your server to recognize them. + "php": ('.php',), + # 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'), +}""", + 'NAVIGATION_LINKS': """{ + DEFAULT_LANG: ( + ("/archive.html", "Archives"), + ("/categories/index.html", "Tags"), + ("/rss.xml", "RSS feed"), + ), }""", 'REDIRECTIONS': [], } +# Generate a list of supported languages here. +# Ugly code follows. +_suplang = {} +_sllength = 0 + +for k, v in LEGAL_VALUES['TRANSLATIONS'].items(): + if not isinstance(k, tuple): + main = k + _suplang[main] = v + else: + main = k[0] + k = k[1:] + bad = [] + good = [] + for i in k: + if i.startswith('!'): + bad.append(i[1:]) + else: + good.append(i) + different = '' + if good or bad: + different += ' [' + if good: + different += 'ALTERNATIVELY ' + ', '.join(good) + if bad: + if good: + different += '; ' + different += 'NOT ' + ', '.join(bad) + if good or bad: + different += ']' + _suplang[main] = v + different + + if len(main) > _sllength: + _sllength = len(main) + +_sllength = str(_sllength) +suplang = (u'# {0:<' + _sllength + u'} {1}\n').format('en', 'English') +del _suplang['en'] +for k, v in sorted(_suplang.items()): + suplang += (u'# {0:<' + _sllength + u'} {1}\n').format(k, v) + +SAMPLE_CONF['_SUPPORTED_LANGUAGES'] = suplang.strip() + +# Generate a list of supported comment systems here. + +SAMPLE_CONF['_SUPPORTED_COMMENT_SYSTEMS'] = '\n'.join(textwrap.wrap( + u', '.join(LEGAL_VALUES['COMMENT_SYSTEM']), + initial_indent=u'# ', subsequent_indent=u'# ', width=79)) + + +def format_default_translations_config(additional_languages): + """Return the string to configure the TRANSLATIONS config variable to + make each additional language visible on the generated site.""" + if not additional_languages: + return SAMPLE_CONF["TRANSLATIONS"] + lang_paths = [' DEFAULT_LANG: "",'] + for lang in sorted(additional_languages): + lang_paths.append(' "{0}": "./{0}",'.format(lang)) + return "{{\n{0}\n}}".format("\n".join(lang_paths)) + + +def format_navigation_links(additional_languages, default_lang, messages): + """Return the string to configure NAVIGATION_LINKS.""" + f = u"""\ + {0}: ( + ("{1}/archive.html", "{2[Archive]}"), + ("{1}/categories/index.html", "{2[Tags]}"), + ("{1}/rss.xml", "{2[RSS feed]}"), + ),""" + + pairs = [] + + def get_msg(lang): + """Generate a smaller messages dict with fallback.""" + fmsg = {} + for i in (u'Archive', u'Tags', u'RSS feed'): + if messages[lang][i]: + fmsg[i] = messages[lang][i] + else: + fmsg[i] = i + return fmsg + + # handle the default language + pairs.append(f.format('DEFAULT_LANG', '', get_msg(default_lang))) + + for l in additional_languages: + pairs.append(f.format(json.dumps(l), '/' + l, get_msg(l))) + + return u'{{\n{0}\n}}'.format('\n\n'.join(pairs)) + # In order to ensure proper escaping, all variables but the three # pre-formatted ones are handled by json.dumps(). @@ -87,7 +197,10 @@ def prepare_config(config): """Parse sample config with JSON.""" p = config.copy() p.update(dict((k, json.dumps(v)) for k, v in p.items() - if k not in ('POSTS', 'PAGES', 'COMPILERS'))) + if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'RSS_READ_MORE_LINK'))) + # READ_MORE_LINKs require some special treatment. + p['INDEX_READ_MORE_LINK'] = "'" + p['INDEX_READ_MORE_LINK'].replace("'", "\\'") + "'" + p['RSS_READ_MORE_LINK'] = "'" + p['RSS_READ_MORE_LINK'].replace("'", "\\'") + "'" return p @@ -97,13 +210,22 @@ class CommandInit(Command): name = "init" - doc_usage = "[--demo] folder" + doc_usage = "[--demo] [--quiet] folder" needs_config = False doc_purpose = "create a Nikola site in the specified folder" cmd_options = [ { + 'name': 'quiet', + 'long': 'quiet', + 'short': 'q', + 'default': False, + 'type': bool, + 'help': "Do not ask questions about config.", + }, + { 'name': 'demo', 'long': 'demo', + 'short': 'd', 'default': False, 'type': bool, 'help': "Create a site filled with example data.", @@ -112,15 +234,12 @@ class CommandInit(Command): @classmethod def copy_sample_site(cls, target): - lib_path = cls.get_path_to_nikola_modules() - src = os.path.join(lib_path, 'data', 'samplesite') + src = resource_filename('nikola', os.path.join('data', 'samplesite')) shutil.copytree(src, target) - fix_git_symlinked(src, target) @classmethod def create_configuration(cls, target): - lib_path = cls.get_path_to_nikola_modules() - template_path = os.path.join(lib_path, 'conf.py.in') + template_path = resource_filename('nikola', 'conf.py.in') conf_template = Template(filename=template_path) conf_path = os.path.join(target, 'conf.py') with codecs.open(conf_path, 'w+', 'utf8') as fd: @@ -132,16 +251,167 @@ class CommandInit(Command): makedirs(os.path.join(target, folder)) @staticmethod - def get_path_to_nikola_modules(): - return os.path.dirname(nikola.__file__) + def ask_questions(target): + """Ask some questions about Nikola.""" + def lhandler(default, toconf, show_header=True): + if show_header: + print("We will now ask you to provide the list of languages you want to use.") + print("Please list all the desired languages, comma-separated, using ISO 639-1 codes. The first language will be used as the default.") + print("Type '?' (a question mark, sans quotes) to list available languages.") + answer = ask('Language(s) to use', 'en') + while answer.strip() == '?': + print('\n# Available languages:') + try: + print(SAMPLE_CONF['_SUPPORTED_LANGUAGES'] + '\n') + except UnicodeEncodeError: + # avoid Unicode characters in supported language names + print(unidecode.unidecode(SAMPLE_CONF['_SUPPORTED_LANGUAGES']) + '\n') + answer = ask('Language(s) to use', 'en') + + langs = [i.strip().lower().replace('-', '_') for i in answer.split(',')] + for partial, full in LEGAL_VALUES['_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS'].items(): + if partial in langs: + langs[langs.index(partial)] = full + print("NOTICE: Assuming '{0}' instead of '{1}'.".format(full, partial)) + + default = langs.pop(0) + SAMPLE_CONF['DEFAULT_LANG'] = default + # format_default_translations_config() is intelligent enough to + # return the current value if there are no additional languages. + SAMPLE_CONF['TRANSLATIONS'] = format_default_translations_config(langs) + + # Get messages for navigation_links. In order to do this, we need + # to generate a throwaway TRANSLATIONS dict. + tr = {default: ''} + for l in langs: + tr[l] = './' + l + # Assuming that base contains all the locales, and that base does + # not inherit from anywhere. + try: + messages = load_messages(['base'], tr, default) + SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages) + except nikola.utils.LanguageNotFoundError as e: + print(" ERROR: the language '{0}' is not supported.".format(e.lang)) + print(" Are you sure you spelled the name correctly? Names are case-sensitive and need to be reproduced as-is (complete with the country specifier, if any).") + print("\nType '?' (a question mark, sans quotes) to list available languages.") + lhandler(default, toconf, show_header=False) + + def tzhandler(default, toconf): + print("\nPlease choose the correct time zone for your blog. Nikola uses the tz database.") + print("You can find your time zone here:") + print("http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") + print("") + answered = False + while not answered: + try: + lz = get_localzone() + except: + lz = None + answer = ask('Time zone', lz if lz else "UTC") + tz = dateutil.tz.gettz(answer) + if tz is not None: + time = datetime.datetime.now(tz).strftime('%H:%M:%S') + print(" Current time in {0}: {1}".format(answer, time)) + answered = ask_yesno("Use this time zone?", True) + else: + print(" ERROR: Time zone not found. Please try again. Time zones are case-sensitive.") + + SAMPLE_CONF['TIMEZONE'] = answer + + def chandler(default, toconf): + print("You can configure comments now. Type '?' (a question mark, sans quotes) to list available comment systems. If you do not want any comments, just leave the field blank.") + answer = ask('Comment system', '') + while answer.strip() == '?': + print('\n# Available comment systems:') + print(SAMPLE_CONF['_SUPPORTED_COMMENT_SYSTEMS']) + print('') + answer = ask('Comment system', '') + + while answer and answer not in LEGAL_VALUES['COMMENT_SYSTEM']: + if answer != '?': + print(' ERROR: Nikola does not know this comment system.') + print('\n# Available comment systems:') + print(SAMPLE_CONF['_SUPPORTED_COMMENT_SYSTEMS']) + print('') + answer = ask('Comment system', '') + + SAMPLE_CONF['COMMENT_SYSTEM'] = answer + SAMPLE_CONF['COMMENT_SYSTEM_ID'] = '' + + if answer: + print("You need to provide the site identifier for your comment system. Consult the Nikola manual for details on what the value should be. (you can leave it empty and come back later)") + answer = ask('Comment system site identifier', '') + SAMPLE_CONF['COMMENT_SYSTEM_ID'] = answer + + STORAGE = {'target': target} + + questions = [ + ('Questions about the site', None, None, None), + # query, default, toconf, destination + ('Destination', None, False, '!target'), + ('Site title', 'My Nikola Site', True, 'BLOG_TITLE'), + ('Site author', 'Nikola Tesla', True, 'BLOG_AUTHOR'), + ('Site author\'s e-mail', 'n.tesla@example.com', True, 'BLOG_EMAIL'), + ('Site description', 'This is a demo site for Nikola.', True, 'BLOG_DESCRIPTION'), + ('Site URL', 'http://getnikola.com/', True, 'SITE_URL'), + ('Questions about languages and locales', None, None, None), + (lhandler, None, True, True), + (tzhandler, None, True, True), + ('Questions about comments', None, None, None), + (chandler, None, True, True), + ] + + print("Creating Nikola Site") + print("====================\n") + print("This is Nikola v{0}. We will now ask you a few easy questions about your new site.".format(nikola.__version__)) + print("If you do not want to answer and want to go with the defaults instead, simply restart with the `-q` parameter.") + + for query, default, toconf, destination in questions: + if target and destination == '!target': + # Skip the destination question if we know it already + pass + else: + if default is toconf is destination is None: + print('--- {0} ---'.format(query)) + elif destination is True: + query(default, toconf) + else: + answer = ask(query, default) + if toconf: + SAMPLE_CONF[destination] = answer + if destination == '!target': + while not answer: + print(' ERROR: you need to specify a target directory.\n') + answer = ask(query, default) + STORAGE['target'] = answer + + print("\nThat's it, Nikola is now configured. Make sure to edit conf.py to your liking.") + print("If you are looking for themes and addons, check out http://themes.getnikola.com/ and http://plugins.getnikola.com/.") + print("Have fun!") + return STORAGE def _execute(self, options={}, args=None): """Create a new site.""" - if not args: - print("Usage: nikola init folder [options]") + try: + target = args[0] + except IndexError: + target = None + if not options.get('quiet'): + st = self.ask_questions(target=target) + try: + if not target: + target = st['target'] + except KeyError: + pass + + if not target: + print("Usage: nikola init [--demo] [--quiet] folder") + print(""" +Options: + -q, --quiet Do not ask questions about config. + -d, --demo Create a site filled with example data.""") return False - target = args[0] - if not options or not options.get('demo'): + if not options.get('demo'): self.create_empty_site(target) LOGGER.info('Created empty site at {0}.'.format(target)) else: diff --git a/nikola/plugins/command/install_plugin.plugin b/nikola/plugins/command/install_plugin.plugin deleted file mode 100644 index 3dbabd8..0000000 --- a/nikola/plugins/command/install_plugin.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index 34223c0..0000000 --- a/nikola/plugins/command/install_plugin.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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.info('Downloading: ' + data[name]) - zip_file = BytesIO() - zip_file.write(requests.get(data[name]).content) - LOGGER.info('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.info('Copying {0} into plugins'.format(plugin_path)) - shutil.copytree(plugin_path, dest_path) - - reqpath = os.path.join(dest_path, 'requirements.txt') - if os.path.exists(reqpath): - LOGGER.notice('This plugin has Python dependencies.') - LOGGER.info('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.info('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. Integrate it with yours in order to make this plugin work!') - print('Contents of the conf.py.sample file:\n') - with codecs.open(confpypath, 'rb', 'utf-8') as fh: - if self.site.colorful: - print(indent(pygments.highlight( - fh.read(), PythonLexer(), TerminalFormatter()), - 4 * ' ')) - else: - print(indent(fh.read(), 4 * ' ')) - return True diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py index 47c73b4..859bd56 100644 --- a/nikola/plugins/command/install_theme.py +++ b/nikola/plugins/command/install_theme.py @@ -87,8 +87,8 @@ class CommandInstallTheme(Command): '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' + "http://themes.getnikola.com/v7/themes.json)", + 'default': 'http://themes.getnikola.com/v7/themes.json' }, ] diff --git a/nikola/plugins/command/mincss.py b/nikola/plugins/command/mincss.py deleted file mode 100644 index 0193458..0000000 --- a/nikola/plugins/command/mincss.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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_page.py b/nikola/plugins/command/new_page.py index 39c0c1d..f07ba39 100644 --- a/nikola/plugins/command/new_page.py +++ b/nikola/plugins/command/new_page.py @@ -59,6 +59,13 @@ class CommandNewPage(Command): 'help': 'Create the page with separate metadata (two file format)' }, { + 'name': 'edit', + 'short': 'e', + 'type': bool, + 'default': False, + 'help': 'Open the page (and meta file, if any) in $EDITOR after creation.' + }, + { 'name': 'content_format', 'short': 'f', 'long': 'format', diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py index cd37a75..42f77cc 100644 --- a/nikola/plugins/command/new_post.py +++ b/nikola/plugins/command/new_post.py @@ -29,8 +29,10 @@ import codecs import datetime import os import sys +import subprocess from blinker import signal +import dateutil.tz from nikola.plugin_categories import Command from nikola import utils @@ -82,7 +84,7 @@ def get_default_compiler(is_post, compilers, post_pages): return 'rest' -def get_date(schedule=False, rule=None, last_date=None, force_today=False): +def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): """Returns a date stamp, given a recurrence rule. schedule - bool: @@ -94,33 +96,45 @@ def get_date(schedule=False, rule=None, last_date=None, force_today=False): 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. + tz - tzinfo: + the timezone used for getting the current time. + + iso8601 - bool: + whether to force ISO 8601 dates (instead of locale-specific ones) + """ - date = now = datetime.datetime.now() + if tz is None: + tz = dateutil.tz.tzlocal() + date = now = datetime.datetime.now(tz) 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 + rrule = None # NOQA 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') + + offset = tz.utcoffset(now) + offset_sec = (offset.days * 24 * 3600 + offset.seconds) + offset_hrs = offset_sec // 3600 + offset_min = offset_sec % 3600 + if iso8601: + tz_str = '{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60) + else: + if offset: + tz_str = ' UTC{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60) + else: + tz_str = ' UTC' + + return date.strftime('%Y-%m-%d %H:%M:%S') + tz_str class CommandNewPost(Command): @@ -168,6 +182,13 @@ class CommandNewPost(Command): 'help': 'Create the post with separate metadata (two file format)' }, { + 'name': 'edit', + 'short': 'e', + 'type': bool, + 'default': False, + 'help': 'Open the post (and meta file, if any) in $EDITOR after creation.' + }, + { 'name': 'content_format', 'short': 'f', 'long': 'format', @@ -242,31 +263,44 @@ class CommandNewPost(Command): print("Creating New {0}".format(content_type.title())) print("-----------------\n") - if title is None: - print("Enter title: ", end='') - # WHY, PYTHON3???? WHY? - sys.stdout.flush() - title = sys.stdin.readline() - else: + if title is not None: print("Title:", title) + else: + while not title: + title = utils.ask('Title') + if isinstance(title, utils.bytes_str): - title = title.decode(sys.stdin.encoding) + try: + title = title.decode(sys.stdin.encoding) + except AttributeError: # for tests + title = title.decode('utf-8') + title = title.strip() if not path: slug = utils.slugify(title) else: if isinstance(path, utils.bytes_str): - path = path.decode(sys.stdin.encoding) + try: + path = path.decode(sys.stdin.encoding) + except AttributeError: # for tests + path = path.decode('utf-8') slug = utils.slugify(os.path.splitext(os.path.basename(path))[0]) # Calculate the date to use for the content 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] + date = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601']) + data = { + 'title': title, + 'slug': slug, + 'date': date, + 'tags': tags, + 'link': '', + 'description': '', + 'type': 'text', + } output_path = os.path.dirname(entry[0]) meta_path = os.path.join(output_path, slug + ".meta") pattern = os.path.basename(entry[0]) @@ -284,19 +318,34 @@ class CommandNewPost(Command): d_name = os.path.dirname(txt_path) utils.makedirs(d_name) metadata = self.site.config['ADDITIONAL_METADATA'] + + # Override onefile if not really supported. + if not compiler_plugin.supports_onefile and onefile: + onefile = False + LOGGER.warn('This compiler does not support one-file posts.') + + content = "Write your {0} here.".format('page' if is_page else 'post') compiler_plugin.create_post( - txt_path, onefile, title=title, + txt_path, content=content, onefile=onefile, title=title, slug=slug, date=date, tags=tags, is_page=is_page, **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 {0} here.".format(content_type)) + fd.write(utils.write_metadata(data)) LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path)) event['meta_path'] = meta_path LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path)) signal('new_' + content_type).send(self, **event) + + if options['edit']: + editor = os.getenv('EDITOR') + to_run = [editor, txt_path] + if not onefile: + to_run.append(meta_path) + if editor: + subprocess.call(to_run) + else: + LOGGER.error('$EDITOR not set, cannot edit the post. Please do it manually.') diff --git a/nikola/plugins/command/planetoid.plugin b/nikola/plugins/command/planetoid.plugin deleted file mode 100644 index e767f31..0000000 --- a/nikola/plugins/command/planetoid.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = planetoid -Module = planetoid - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -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 deleted file mode 100644 index fe1a59b..0000000 --- a/nikola/plugins/command/planetoid/__init__.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 datetime -import hashlib -from optparse import OptionParser -import os -import sys - -from doit.tools import timeout -from nikola.plugin_categories import Command, Task -from nikola.utils import config_changed, req_missing, get_logger, STDERR_HANDLER - -LOGGER = get_logger('planetoid', STDERR_HANDLER) - -try: - import feedparser -except ImportError: - feedparser = None # NOQA - -try: - import peewee -except ImportError: - peewee = None - - -if peewee is not None: - class Feed(peewee.Model): - name = peewee.CharField() - url = peewee.CharField(max_length=200) - last_status = peewee.CharField(null=True) - etag = peewee.CharField(max_length=200) - last_modified = peewee.DateTimeField() - - class Entry(peewee.Model): - date = peewee.DateTimeField() - feed = peewee.ForeignKeyField(Feed) - content = peewee.TextField(max_length=20000) - link = peewee.CharField(max_length=200) - title = peewee.CharField(max_length=200) - guid = peewee.CharField(max_length=200) - - -class Planetoid(Command, Task): - """Maintain a planet-like thing.""" - name = "planetoid" - - def init_db(self): - # setup database - Feed.create_table(fail_silently=True) - Entry.create_table(fail_silently=True) - - def gen_tasks(self): - if peewee is None or sys.version_info[0] == 3: - if sys.version_info[0] == 3: - message = 'Peewee, a requirement of the "planetoid" command, is currently incompatible with Python 3.' - else: - req_missing('peewee', 'use the "planetoid" command') - message = '' - yield { - 'basename': self.name, - 'name': '', - 'verbosity': 2, - 'actions': ['echo "%s"' % message] - } - else: - self.init_db() - self.load_feeds() - for task in self.task_update_feeds(): - yield task - for task in self.task_generate_posts(): - yield task - yield { - 'basename': self.name, - 'name': '', - 'actions': [], - 'file_dep': ['feeds'], - 'task_dep': [ - self.name + "_fetch_feed", - self.name + "_generate_posts", - ] - } - - def run(self, *args): - parser = OptionParser(usage="nikola %s [options]" % self.name) - (options, args) = parser.parse_args(list(args)) - - def load_feeds(self): - "Read the feeds file, add it to the database." - feeds = [] - feed = name = None - for line in codecs.open('feeds', 'r', 'utf-8'): - line = line.strip() - if line.startswith("#"): - continue - elif line.startswith('http'): - feed = line - elif line: - name = line - if feed and name: - feeds.append([feed, name]) - feed = name = None - - def add_feed(name, url): - f = Feed.create( - name=name, - url=url, - etag='foo', - last_modified=datetime.datetime(1970, 1, 1), - ) - f.save() - - def update_feed_url(feed, url): - feed.url = url - feed.save() - - for feed, name in feeds: - f = Feed.select().where(Feed.name == name) - if not list(f): - add_feed(name, feed) - elif list(f)[0].url != feed: - update_feed_url(list(f)[0], feed) - - def task_update_feeds(self): - """Download feed contents, add entries to the database.""" - def update_feed(feed): - modified = feed.last_modified.timetuple() - etag = feed.etag - try: - parsed = feedparser.parse( - feed.url, - etag=etag, - modified=modified - ) - feed.last_status = str(parsed.status) - except: # Probably a timeout - # TODO: log failure - return - if parsed.feed.get('title'): - LOGGER.info(parsed.feed.title) - else: - LOGGER.info(feed.url) - feed.etag = parsed.get('etag', 'foo') - modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6] - LOGGER.info("==========>", modified) - modified = datetime.datetime(*modified) - feed.last_modified = modified - feed.save() - # No point in adding items from missinfg feeds - if parsed.status > 400: - # TODO log failure - return - for entry_data in parsed.entries: - LOGGER.info("=========================================") - date = entry_data.get('published_parsed', None) - if date is None: - date = entry_data.get('updated_parsed', None) - if date is None: - LOGGER.error("Can't parse date from:\n", entry_data) - return False - LOGGER.info("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) - if content: - content = content[0].value - if not content: - content = entry_data.get('description', None) - if not content: - content = entry_data.get('summary', 'Sin contenido') - guid = str(entry_data.get('guid', entry_data.link)) - link = entry_data.link - LOGGER.info(repr([date, title])) - e = list(Entry.select().where(Entry.guid == guid)) - LOGGER.info( - repr(dict( - date=date, - title=title, - content=content, - guid=guid, - feed=feed, - link=link, - )) - ) - if not e: - entry = Entry.create( - date=date, - title=title, - content=content, - guid=guid, - feed=feed, - link=link, - ) - else: - entry = e[0] - entry.date = date - entry.title = title - entry.content = content - entry.link = link - entry.save() - flag = False - for feed in Feed.select(): - flag = True - task = { - 'basename': self.name + "_fetch_feed", - 'name': str(feed.url), - 'actions': [(update_feed, (feed, ))], - 'uptodate': [timeout(datetime.timedelta(minutes= - self.site.config.get('PLANETOID_REFRESH', 60)))], - } - yield task - if not flag: - yield { - 'basename': self.name + "_fetch_feed", - 'name': '', - 'actions': [], - } - - def task_generate_posts(self): - """Generate post files for the blog entries.""" - def gen_id(entry): - h = hashlib.md5() - h.update(entry.feed.name.encode('utf8')) - h.update(entry.guid) - return h.hexdigest() - - def generate_post(entry): - unique_id = gen_id(entry) - meta_path = os.path.join('posts', unique_id + '.meta') - post_path = os.path.join('posts', unique_id + '.txt') - with codecs.open(meta_path, 'wb+', 'utf8') as fd: - fd.write('%s\n' % entry.title.replace('\n', ' ')) - fd.write('%s\n' % unique_id) - fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M')) - fd.write('\n') - fd.write('%s\n' % entry.link) - with codecs.open(post_path, 'wb+', 'utf8') as fd: - fd.write('.. raw:: html\n\n') - content = entry.content - if not content: - content = 'Sin contenido' - for line in content.splitlines(): - fd.write(' %s\n' % line) - - if not os.path.isdir('posts'): - os.mkdir('posts') - flag = False - for entry in Entry.select().order_by(Entry.date.desc()): - flag = True - entry_id = gen_id(entry) - yield { - 'basename': self.name + "_generate_posts", - 'targets': [os.path.join('posts', entry_id + '.meta'), os.path.join('posts', entry_id + '.txt')], - 'name': entry_id, - 'actions': [(generate_post, (entry,))], - 'uptodate': [config_changed({1: entry})], - 'task_dep': [self.name + "_fetch_feed"], - } - if not flag: - yield { - 'basename': self.name + "_generate_posts", - 'name': '', - 'actions': [], - } diff --git a/nikola/plugins/command/plugin.plugin b/nikola/plugins/command/plugin.plugin new file mode 100644 index 0000000..d2bca92 --- /dev/null +++ b/nikola/plugins/command/plugin.plugin @@ -0,0 +1,10 @@ +[Core] +Name = plugin +Module = plugin + +[Documentation] +Author = Roberto Alsina and Chris Warrick +Version = 0.2 +Website = http://getnikola.com +Description = Manage Nikola plugins + diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py new file mode 100644 index 0000000..df0e7a4 --- /dev/null +++ b/nikola/plugins/command/plugin.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2014 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 +from io import BytesIO +import os +import shutil +import subprocess +import sys + +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('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 CommandPlugin(Command): + """Manage plugins.""" + + json = None + name = "plugin" + doc_usage = "[[-u][--user] --install name] | [[-u] [-l |--upgrade|--list-installed] | [--uninstall name]]" + doc_purpose = "manage plugins" + output_dir = None + needs_config = False + cmd_options = [ + { + 'name': 'install', + 'short': 'i', + 'long': 'install', + 'type': str, + 'default': '', + 'help': 'Install a plugin.', + }, + { + 'name': 'uninstall', + 'long': 'uninstall', + 'short': 'r', + 'type': str, + 'default': '', + 'help': 'Uninstall a plugin.' + }, + { + '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/v7/plugins.json)", + 'default': 'http://plugins.getnikola.com/v7/plugins.json' + }, + { + 'name': 'user', + 'long': 'user', + 'type': bool, + 'help': "Install user-wide, available for all sites.", + 'default': False + }, + { + 'name': 'upgrade', + 'long': 'upgrade', + 'type': bool, + 'help': "Upgrade all installed plugins.", + 'default': False + }, + { + 'name': 'list_installed', + 'long': 'list-installed', + 'type': bool, + 'help': "List the installed plugins with their location.", + 'default': False + }, + ] + + def _execute(self, options, args): + """Install plugin into current site.""" + url = options['url'] + user_mode = options['user'] + + # See the "mode" we need to operate in + install = options.get('install') + uninstall = options.get('uninstall') + upgrade = options.get('upgrade') + list_available = options.get('list') + list_installed = options.get('list_installed') + command_count = [bool(x) for x in ( + install, + uninstall, + upgrade, + list_available, + list_installed)].count(True) + if command_count > 1 or command_count == 0: + print(self.help()) + return + + if not self.site.configured and not user_mode and install: + LOGGER.notice('No site found, assuming --user') + user_mode = True + + if user_mode: + self.output_dir = os.path.expanduser('~/.nikola/plugins') + else: + self.output_dir = 'plugins' + + if list_available: + self.list_available(url) + elif list_installed: + self.list_installed() + elif upgrade: + self.do_upgrade(url) + elif uninstall: + self.do_uninstall(uninstall) + elif install: + self.do_install(url, install) + + def list_available(self, url): + data = self.get_json(url) + print("Available Plugins:") + print("------------------") + for plugin in sorted(data.keys()): + print(plugin) + return True + + def list_installed(self): + plugins = [] + for plugin in self.site.plugin_manager.getAllPlugins(): + p = plugin.path + if os.path.isdir(p): + p = p + os.sep + else: + p = p + '.py' + plugins.append([plugin.name, p]) + + plugins.sort() + for name, path in plugins: + print('{0} at {1}'.format(name, path)) + + def do_upgrade(self, url): + LOGGER.warning('This is not very smart, it just reinstalls some plugins and hopes for the best') + data = self.get_json(url) + plugins = [] + for plugin in self.site.plugin_manager.getAllPlugins(): + p = plugin.path + if os.path.isdir(p): + p = p + os.sep + else: + p = p + '.py' + if plugin.name in data: + plugins.append([plugin.name, p]) + print('Will upgrade {0} plugins: {1}'.format(len(plugins), ', '.join(n for n, _ in plugins))) + for name, path in plugins: + print('Upgrading {0}'.format(name)) + p = path + while True: + tail, head = os.path.split(path) + if head == 'plugins': + self.output_dir = path + break + elif tail == '': + LOGGER.error("Can't find the plugins folder for path: {0}".format(p)) + return False + else: + path = tail + self.do_install(url, name) + + def do_install(self, url, name): + data = self.get_json(url) + if name in data: + utils.makedirs(self.output_dir) + LOGGER.info('Downloading: ' + data[name]) + zip_file = BytesIO() + zip_file.write(requests.get(data[name]).content) + LOGGER.info('Extracting: {0} into {1}/'.format(name, self.output_dir)) + utils.extract_all(zip_file, self.output_dir) + dest_path = os.path.join(self.output_dir, 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.info('Copying {0} into plugins'.format(plugin_path)) + shutil.copytree(plugin_path, dest_path) + + reqpath = os.path.join(dest_path, 'requirements.txt') + if os.path.exists(reqpath): + LOGGER.notice('This plugin has Python dependencies.') + LOGGER.info('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.info('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. Integrate it with yours in order to make this plugin work!') + print('Contents of the conf.py.sample file:\n') + with codecs.open(confpypath, 'rb', 'utf-8') as fh: + if self.site.colorful: + print(indent(pygments.highlight( + fh.read(), PythonLexer(), TerminalFormatter()), + 4 * ' ')) + else: + print(indent(fh.read(), 4 * ' ')) + return True + + def do_uninstall(self, name): + for plugin in self.site.plugin_manager.getAllPlugins(): # FIXME: this is repeated thrice + p = plugin.path + if os.path.isdir(p): + p = p + os.sep + else: + p = os.path.dirname(p) + if name == plugin.name: # Uninstall this one + LOGGER.warning('About to uninstall plugin: {0}'.format(name)) + LOGGER.warning('This will delete {0}'.format(p)) + inpf = raw_input if sys.version_info[0] == 2 else input + sure = inpf('Are you sure? [y/n] ') + if sure.lower().startswith('y'): + LOGGER.warning('Removing {0}'.format(p)) + shutil.rmtree(p) + return True + LOGGER.error('Unknown plugin: {0}'.format(name)) + return False + + def get_json(self, url): + if requests is None: + utils.req_missing(['requests'], 'install or list available plugins', python=True, optional=False) + if self.json is None: + self.json = requests.get(url).json() + return self.json diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index f27d1f7..623e2db 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -89,7 +89,11 @@ class CommandServe(Command): server_url = "http://{0}:{1}/".format(options['address'], options['port']) self.logger.info("Opening {0} in the default web browser ...".format(server_url)) webbrowser.open(server_url) - httpd.serve_forever() + try: + httpd.serve_forever() + except KeyboardInterrupt: + self.logger.info("Server is shutting down.") + exit(130) class OurHTTPRequestHandler(SimpleHTTPRequestHandler): diff --git a/nikola/plugins/compile/asciidoc.plugin b/nikola/plugins/compile/asciidoc.plugin deleted file mode 100644 index 47c5608..0000000 --- a/nikola/plugins/compile/asciidoc.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index 68f96d9..0000000 --- a/nikola/plugins/compile/asciidoc.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - - -class CompileAsciiDoc(PageCompiler): - """Compile asciidoc into HTML.""" - - name = "asciidoc" - demote_headers = True - - 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, is_page=False, **kw): - metadata = OrderedDict() - 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 {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/bbcode.py b/nikola/plugins/compile/bbcode.py deleted file mode 100644 index 0961ffe..0000000 --- a/nikola/plugins/compile/bbcode.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 bbcode.""" - -import codecs -import os -import re - -try: - import bbcode -except ImportError: - bbcode = None # NOQA - -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - - -class CompileBbcode(PageCompiler): - """Compile bbcode into HTML.""" - - name = "bbcode" - - def __init__(self): - if bbcode is None: - return - self.parser = bbcode.Parser() - self.parser.add_simple_formatter("note", "") - - def compile_html(self, source, dest, is_two_file=True): - if bbcode is None: - 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) - - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - 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('[note]<!--\n') - for k, v in metadata.items(): - fd.write('.. {0}: {1}\n'.format(k, v)) - fd.write('-->[/note]\n\n') - fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py index 09a9756..fff7f89 100644 --- a/nikola/plugins/compile/html.py +++ b/nikola/plugins/compile/html.py @@ -31,12 +31,7 @@ import re import codecs from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA +from nikola.utils import makedirs, write_metadata _META_SEPARATOR = '(' + os.linesep * 2 + '|' + ('\n' * 2) + '|' + ("\r\n" * 2) + ')' @@ -56,15 +51,20 @@ class CompileHtml(PageCompiler): out_file.write(data) return True - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() + def create_post(self, path, **kw): + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + metadata = {} metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' 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(write_metadata(metadata)) fd.write('-->\n\n') - fd.write("\n<p>Write your {0} here.</p>\n".format('page' if is_page else 'post')) + fd.write(content) diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin index 3d15bb0..e258d8a 100644 --- a/nikola/plugins/compile/ipynb.plugin +++ b/nikola/plugins/compile/ipynb.plugin @@ -3,7 +3,7 @@ Name = ipynb Module = ipynb [Documentation] -Author = Damián Avila +Author = Damian Avila Version = 1.0 Website = http://www.oquanta.info Description = Compile IPython notebooks into HTML diff --git a/nikola/plugins/compile/ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py index 2b1fd28..f4d554c 100644 --- a/nikola/plugins/compile/ipynb/__init__.py +++ b/nikola/plugins/compile/ipynb/__init__.py @@ -41,16 +41,12 @@ except ImportError: from nikola.plugin_categories import PageCompiler from nikola.utils import makedirs, req_missing -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - class CompileIPynb(PageCompiler): """Compile IPynb into HTML.""" name = "ipynb" + supports_onefile = False def compile_html(self, source, dest, is_two_file=True): if flag is None: @@ -66,19 +62,15 @@ class CompileIPynb(PageCompiler): (body, resources) = exportHtml.from_notebook_node(nb_json) out_file.write(body) - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - metadata.update(self.default_metadata) - metadata.update(kw) - d_name = os.path.dirname(path) + def create_post(self, path, **kw): + # content and onefile are ignored by ipynb. + kw.pop('content', None) + onefile = kw.pop('onefile', False) + kw.pop('is_page', False) + makedirs(os.path.dirname(path)) - meta_path = os.path.join(d_name, kw['slug'] + ".meta") - with codecs.open(meta_path, "wb+", "utf8") as fd: - fd.write('\n'.join((metadata['title'], metadata['slug'], - metadata['date'], metadata['tags'], - metadata['link'], - metadata['description'], metadata['type']))) - print("Your {0}'s metadata is at: {1}".format('page' if is_page else 'post', meta_path)) + if onefile: + raise Exception('The one-file format is not supported by this compiler.') with codecs.open(path, "wb+", "utf8") as fd: fd.write("""{ "metadata": { diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index d0fa66a..4182626 100644 --- a/nikola/plugins/compile/markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -34,30 +34,14 @@ import re try: from markdown import markdown - - from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension - nikola_extension = NikolaExtension() - - from nikola.plugins.compile.markdown.mdx_gist import GistExtension - gist_extension = GistExtension() - - from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension - podcast_extension = PodcastExtension() - except ImportError: markdown = None # NOQA nikola_extension = None gist_extension = None podcast_extension = None - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing +from nikola.utils import makedirs, req_missing, write_metadata class CompileMarkdown(PageCompiler): @@ -65,9 +49,22 @@ class CompileMarkdown(PageCompiler): name = "markdown" demote_headers = True - extensions = [gist_extension, nikola_extension, podcast_extension] + extensions = [] site = None + def set_site(self, site): + for plugin_info in site.plugin_manager.getPluginsOfCategory("MarkdownExtension"): + if plugin_info.name in site.config['DISABLED_PLUGINS']: + site.plugin_manager.removePluginFromCategory(plugin_info, "MarkdownExtension") + continue + + site.plugin_manager.activatePluginByName(plugin_info.name) + plugin_info.plugin_object.set_site(site) + self.extensions.append(plugin_info.plugin_object) + plugin_info.plugin_object.short_help = plugin_info.description + + return super(CompileMarkdown, self).set_site(site) + def compile_html(self, source, dest, is_two_file=True): if markdown is None: req_missing(['markdown'], 'build this site (compile Markdown)') @@ -81,15 +78,21 @@ class CompileMarkdown(PageCompiler): output = markdown(data, self.extensions) out_file.write(output) - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() + def create_post(self, path, **kw): + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + + metadata = {} metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' 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(write_metadata(metadata)) fd.write('-->\n\n') - fd.write("Write your {0} here.".format('page' if is_page else 'post')) + fd.write(content) diff --git a/nikola/plugins/compile/bbcode.plugin b/nikola/plugins/compile/markdown/mdx_gist.plugin index b3d9357..0e5c578 100644 --- a/nikola/plugins/compile/bbcode.plugin +++ b/nikola/plugins/compile/markdown/mdx_gist.plugin @@ -1,10 +1,9 @@ [Core] -Name = bbcode -Module = bbcode +Name = mdx_gist +Module = mdx_gist [Documentation] Author = Roberto Alsina Version = 0.1 Website = http://getnikola.com -Description = Compile BBCode into HTML - +Description = Extension for embedding gists diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index d92295d..247478b 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -117,10 +117,18 @@ Error Case: non-existent file: ''' 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 + +try: + from markdown.extensions import Extension + from markdown.inlinepatterns import Pattern + from markdown.util import AtomicString + from markdown.util import etree +except ImportError: + # No need to catch this, if you try to use this without Markdown, + # the markdown compiler will fail first + Extension = Pattern = object + +from nikola.plugin_categories import MarkdownExtension from nikola.utils import get_logger, req_missing, STDERR_HANDLER LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER) @@ -209,7 +217,7 @@ class GistPattern(Pattern): return gist_elem -class GistExtension(Extension): +class GistExtension(MarkdownExtension, Extension): def __init__(self, configs={}): # set extension defaults self.config = {} diff --git a/nikola/plugins/command/mincss.plugin b/nikola/plugins/compile/markdown/mdx_nikola.plugin index d394d06..7af52a4 100644 --- a/nikola/plugins/command/mincss.plugin +++ b/nikola/plugins/compile/markdown/mdx_nikola.plugin @@ -1,10 +1,9 @@ [Core] -Name = mincss -Module = mincss +Name = mdx_nikola +Module = mdx_nikola [Documentation] Author = Roberto Alsina Version = 0.1 Website = http://getnikola.com -Description = Apply mincss to the generated site - +Description = Nikola-specific Markdown extensions diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py index b7c29a5..ca67511 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.py +++ b/nikola/plugins/compile/markdown/mdx_nikola.py @@ -27,23 +27,31 @@ """Markdown Extension for Nikola-specific post-processing""" from __future__ import unicode_literals import re -from markdown.postprocessors import Postprocessor -from markdown.extensions import Extension +try: + from markdown.postprocessors import Postprocessor + from markdown.extensions import Extension +except ImportError: + # No need to catch this, if you try to use this without Markdown, + # the markdown compiler will fail first + Postprocessor = Extension = object + +from nikola.plugin_categories import MarkdownExtension + +# FIXME: duplicated with listings.py +CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL) class NikolaPostProcessor(Postprocessor): def run(self, text): output = text - # python-markdown's highlighter uses the class 'codehilite' to wrap - # code, instead of the standard 'code'. None of the standard - # pygments stylesheets use this class, so swap it to be 'code' - output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)', - r'\1code\2', output) + # python-markdown's highlighter uses <div class="codehilite"><pre> + # for code. We switch it to reST's <pre class="code">. + output = CODERE.sub('<pre class="code literal-block">\\1</pre>', output) return output -class NikolaExtension(Extension): +class NikolaExtension(MarkdownExtension, Extension): def extendMarkdown(self, md, md_globals): pp = NikolaPostProcessor() md.postprocessors.add('nikola_post_processor', pp, '_end') diff --git a/nikola/plugins/compile/markdown/mdx_podcast.plugin b/nikola/plugins/compile/markdown/mdx_podcast.plugin new file mode 100644 index 0000000..dc16044 --- /dev/null +++ b/nikola/plugins/compile/markdown/mdx_podcast.plugin @@ -0,0 +1,9 @@ +[Core] +Name = mdx_podcast +Module = mdx_podcast + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Markdown extensions for embedding podcasts and other audio files diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py index b38b969..9a67910 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.py +++ b/nikola/plugins/compile/markdown/mdx_podcast.py @@ -39,9 +39,15 @@ Basic Example: <p><audio src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3"></audio></p> ''' -from markdown.extensions import Extension -from markdown.inlinepatterns import Pattern -from markdown.util import etree +from nikola.plugin_categories import MarkdownExtension +try: + from markdown.extensions import Extension + from markdown.inlinepatterns import Pattern + from markdown.util import etree +except ImportError: + # No need to catch this, if you try to use this without Markdown, + # the markdown compiler will fail first + Pattern = Extension = object PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]' @@ -62,7 +68,7 @@ class PodcastPattern(Pattern): return audio_elem -class PodcastExtension(Extension): +class PodcastExtension(MarkdownExtension, Extension): def __init__(self, configs={}): # set extension defaults self.config = {} diff --git a/nikola/plugins/compile/misaka.plugin b/nikola/plugins/compile/misaka.plugin deleted file mode 100644 index fef6d71..0000000 --- a/nikola/plugins/compile/misaka.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = misaka -Module = misaka - -[Documentation] -Author = Chris Lee -Version = 0.1 -Website = http://c133.org/ -Description = Compile Markdown into HTML with Mikasa instead of python-markdown - diff --git a/nikola/plugins/compile/misaka.py b/nikola/plugins/compile/misaka.py deleted file mode 100644 index 4951c9f..0000000 --- a/nikola/plugins/compile/misaka.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2013-2014 Chris Lee 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 misaka.""" - -from __future__ import unicode_literals - -import codecs -import os -import re - -try: - import misaka -except ImportError: - misaka = None # NOQA - nikola_extension = None -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - - gist_extension = None - podcast_extension = None - -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing - - -class CompileMisaka(PageCompiler): - """Compile Misaka into HTML.""" - - name = "misaka" - demote_headers = True - - def __init__(self, *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, is_two_file=True): - if misaka is None: - 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) - - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - 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("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 654c7c8..6aa737e 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -35,12 +35,7 @@ import os import subprocess from nikola.plugin_categories import PageCompiler -from nikola.utils import req_missing, makedirs - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA +from nikola.utils import req_missing, makedirs, write_metadata class CompilePandoc(PageCompiler): @@ -56,15 +51,20 @@ class CompilePandoc(PageCompiler): 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, is_page=False, **kw): - metadata = OrderedDict() + def create_post(self, path, **kw): + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + metadata = {} metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' 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(write_metadata(metadata)) fd.write('-->\n\n') - fd.write("Write your {0} here.".format('page' if is_page else 'post')) + fd.write(content) diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index 0a652a6..601f098 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -33,12 +33,7 @@ import shutil import codecs from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA +from nikola.utils import makedirs, write_metadata class CompilePhp(PageCompiler): @@ -50,18 +45,23 @@ class CompilePhp(PageCompiler): makedirs(os.path.dirname(dest)) shutil.copyfile(source, dest) - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() + def create_post(self, path, **kw): + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + metadata = {} metadata.update(self.default_metadata) metadata.update(kw) os.makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' 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(write_metadata(metadata)) fd.write('-->\n\n') - fd.write("\n<p>Write your {0} here.</p>".format('page' if is_page else 'post')) + fd.write(content) def extension(self): return ".php" diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py index 9a4e19b..a93199c 100644 --- a/nikola/plugins/compile/rest/__init__.py +++ b/nikola/plugins/compile/rest/__init__.py @@ -40,13 +40,8 @@ try: except ImportError: has_docutils = False -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - from nikola.plugin_categories import PageCompiler -from nikola.utils import get_logger, makedirs, req_missing +from nikola.utils import get_logger, makedirs, req_missing, write_metadata class CompileRest(PageCompiler): @@ -102,22 +97,25 @@ class CompileRest(PageCompiler): else: return False - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() + def create_post(self, path, **kw): + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + metadata = {} metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' 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 {0} here.".format('page' if is_page else 'post')) + fd.write(write_metadata(metadata)) + fd.write('\n' + content) 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'])): + if plugin_info.name in site.config['DISABLED_PLUGINS']: site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension") continue diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py index 03878a3..55ddf5c 100644 --- a/nikola/plugins/compile/rest/chart.py +++ b/nikola/plugins/compile/rest/chart.py @@ -37,13 +37,16 @@ except ImportError: from nikola.plugin_categories import RestExtension from nikola.utils import req_missing +_site = None + class Plugin(RestExtension): name = "rest_chart" def set_site(self, site): - self.site = site + global _site + _site = self.site = site directives.register_directive('chart', Chart) return super(Plugin, self).set_site(site) @@ -146,5 +149,9 @@ class Chart(Directive): 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')] + data = chart.render().decode('utf8') + if _site and _site.invariant: + import re + data = re.sub('id="chart-[a-f0-9\-]+"', 'id="chart-foobar"', data) + data = re.sub('#chart-[a-f0-9\-]+', '#chart-foobar', data) + return [nodes.raw('', data, format='html')] diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py index a150a81..6143606 100644 --- a/nikola/plugins/compile/rest/doc.py +++ b/nikola/plugins/compile/rest/doc.py @@ -48,7 +48,6 @@ def doc_role(name, rawtext, text, lineno, inliner, # 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 @@ -73,7 +72,6 @@ def doc_role(name, rawtext, text, lineno, inliner, 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( diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index d70e02d..18a1807 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -46,6 +46,7 @@ except ImportError: # docutils < 0.9 (Debian Sid For The Loss) class CodeBlock(Directive): required_arguments = 1 has_content = True + option_spec = {} CODE = '<pre>{0}</pre>' def run(self): diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py index 6804b58..456e571 100644 --- a/nikola/plugins/compile/rest/post_list.py +++ b/nikola/plugins/compile/rest/post_list.py @@ -124,7 +124,10 @@ class PostList(Directive): 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) + if self.site.invariant: # for testing purposes + post_list_id = self.options.get('id', 'post_list_' + 'fixedvaluethatisnotauuid') + else: + post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex) posts = [] step = -1 if reverse is None else None diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py index 203ae51..ea8e413 100644 --- a/nikola/plugins/compile/rest/slides.py +++ b/nikola/plugins/compile/rest/slides.py @@ -53,12 +53,17 @@ class Slides(Directive): if len(self.content) == 0: return + if self.site.invariant: # for testing purposes + carousel_id = 'slides_' + 'fixedvaluethatisnotauuid' + else: + carousel_id = 'slides_' + uuid.uuid4().hex + output = self.site.template_system.render_template( 'slides.tmpl', None, { - 'content': self.content, - 'carousel_id': 'slides_' + uuid.uuid4().hex, + 'slides_content': self.content, + 'carousel_id': carousel_id, } ) return [nodes.raw('', output, format='html')] diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index 82c4dc1..4b34dfe 100644 --- a/nikola/plugins/compile/rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -49,9 +49,9 @@ class Plugin(RestExtension): return super(Plugin, self).set_site(site) -CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}" +CODE = """<iframe src="//player.vimeo.com/video/{vimeo_id}" width="{width}" height="{height}" -frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen> +frameborder="0" webkitAllowFullScreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowFullScreen="allowFullScreen"> </iframe> """ @@ -108,7 +108,7 @@ class Vimeo(Directive): if json: # we can attempt to retrieve video attributes from vimeo try: - url = ('http://vimeo.com/api/v2/video/{0}' + url = ('//vimeo.com/api/v2/video/{0}' '.json'.format(self.arguments[0])) data = requests.get(url).text video_attributes = json.loads(data)[0] diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py index 19e12d1..b32e77a 100644 --- a/nikola/plugins/compile/rest/youtube.py +++ b/nikola/plugins/compile/rest/youtube.py @@ -44,7 +44,7 @@ class Plugin(RestExtension): CODE = """\ <iframe width="{width}" height="{height}" -src="http://www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" +src="//www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" ></iframe>""" diff --git a/nikola/plugins/compile/textile.plugin b/nikola/plugins/compile/textile.plugin deleted file mode 100644 index 6439b0f..0000000 --- a/nikola/plugins/compile/textile.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = textile -Module = textile - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Compile Textile into HTML - diff --git a/nikola/plugins/compile/textile.py b/nikola/plugins/compile/textile.py deleted file mode 100644 index 1679831..0000000 --- a/nikola/plugins/compile/textile.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 textile.""" - -import codecs -import os -import re - -try: - from textile import textile -except ImportError: - textile = None # NOQA - -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - - -class CompileTextile(PageCompiler): - """Compile textile into HTML.""" - - name = "textile" - demote_headers = True - - def compile_html(self, source, dest, is_two_file=True): - if textile is None: - 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) - - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - 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('<notextile> <!--\n') - for k, v in metadata.items(): - fd.write('.. {0}: {1}\n'.format(k, v)) - fd.write('--></notextile>\n\n') - fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/txt2tags.plugin b/nikola/plugins/compile/txt2tags.plugin deleted file mode 100644 index 55eb0a0..0000000 --- a/nikola/plugins/compile/txt2tags.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = txt2tags -Module = txt2tags - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Compile Txt2tags into HTML - diff --git a/nikola/plugins/compile/txt2tags.py b/nikola/plugins/compile/txt2tags.py deleted file mode 100644 index bb6afa5..0000000 --- a/nikola/plugins/compile/txt2tags.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 txt2tags. - -Txt2tags is not in PyPI, you can install it with - -easy_install -f "http://txt2tags.org/txt2tags.py#egg=txt2tags-2.6" txt2tags - -""" - -import codecs -import os - -try: - from txt2tags import exec_command_line as txt2tags -except ImportError: - txt2tags = None # NOQA - -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing - - -class CompileTxt2tags(PageCompiler): - """Compile txt2tags into HTML.""" - - name = "txt2tags" - demote_headers = True - - def compile_html(self, source, dest, is_two_file=True): - if txt2tags is None: - req_missing(['txt2tags'], 'build this site (compile txt2tags)') - makedirs(os.path.dirname(dest)) - cmd = ["-t", "html", "--no-headers", "--outfile", dest, source] - txt2tags(cmd) - - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - 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'''\n<!--\n") - for k, v in metadata.items(): - fd.write('.. {0}: {1}\n'.format(k, v)) - fd.write("-->\n'''\n") - fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/wiki.plugin b/nikola/plugins/compile/wiki.plugin deleted file mode 100644 index eee14a8..0000000 --- a/nikola/plugins/compile/wiki.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = wiki -Module = wiki - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Compile WikiMarkup into HTML - diff --git a/nikola/plugins/compile/wiki.py b/nikola/plugins/compile/wiki.py deleted file mode 100644 index f4858c7..0000000 --- a/nikola/plugins/compile/wiki.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 CreoleWiki.""" - -import codecs -import os - -try: - from creole import Parser - from creole.html_emitter import HtmlEmitter - creole = True -except ImportError: - creole = None - -from nikola.plugin_categories import PageCompiler -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict # NOQA - -from nikola.utils import makedirs, req_missing - - -class CompileWiki(PageCompiler): - """Compile CreoleWiki into HTML.""" - - name = "wiki" - demote_headers = True - - def compile_html(self, source, dest, is_two_file=True): - if creole is None: - 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() - document = Parser(data).parse() - output = HtmlEmitter(document).emit() - out_file.write(output) - - def create_post(self, path, onefile=False, is_page=False, **kw): - metadata = OrderedDict() - metadata.update(self.default_metadata) - metadata.update(kw) - 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 ' - 'option.') - with codecs.open(path, "wb+", "utf8") as fd: - fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py index fdc892e..593c381 100644 --- a/nikola/plugins/loghandler/stderr.py +++ b/nikola/plugins/loghandler/stderr.py @@ -41,7 +41,11 @@ class StderrHandler(SignalHandler): conf = self.site.config.get('LOGGING_HANDLERS').get('stderr') if conf or os.getenv('NIKOLA_DEBUG'): self.site.loghandlers.append(ColorfulStderrHandler( - level='DEBUG' if DEBUG else conf.get('loglevel', 'WARNING').upper(), + # We do not allow the level to be something else than 'DEBUG' + # or 'INFO' Any other level can have bad effects on the user + # experience and is discouraged. + # (oh, and it was incorrectly set to WARNING before) + level='DEBUG' if DEBUG or (conf.get('loglevel', 'INFO').upper() == 'DEBUG') else 'INFO', format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}' )) diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index a65a63f..4f1ab19 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -73,16 +73,15 @@ class Archive(Task): context["permalink"] = self.site.link("archive", year, lang) if not kw["create_monthly_archive"]: template_name = "list_post.tmpl" - post_list = [self.site.global_data[post] for post in posts] - post_list.sort(key=lambda a: a.date) + post_list = sorted(posts, key=lambda a: a.date) post_list.reverse() context["posts"] = post_list else: # Monthly archives, just list the months - months = set([m.split('/')[1] for m in self.site.posts_per_month.keys() if m.startswith(str(year))]) + months = set([(m.split('/')[1], self.site.link("archive", m, lang)) for m in self.site.posts_per_month.keys() if m.startswith(str(year))]) months = sorted(list(months)) months.reverse() template_name = "list.tmpl" - context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), month] for month in months] + context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link] for month, link in months] post_list = [] task = self.site.generic_post_list_renderer( lang, @@ -93,7 +92,12 @@ class Archive(Task): context, ) n = len(post_list) if 'posts' in context else len(months) - task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: n} + + deps_translatable = {} + for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: + deps_translatable[k] = self.site.GLOBAL_CONTEXT[k](lang) + + task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: n, 4: deps_translatable} task['uptodate'] = [config_changed(task_cfg)] task['basename'] = self.name yield task @@ -106,8 +110,7 @@ class Archive(Task): kw['output_folder'], self.site.path("archive", yearmonth, lang)) year, month = yearmonth.split('/') - post_list = [self.site.global_data[post] for post in posts] - post_list.sort(key=lambda a: a.date) + post_list = sorted(posts, key=lambda a: a.date) post_list.reverse() context = {} context["lang"] = lang @@ -141,8 +144,8 @@ class Archive(Task): 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["items"] = [(y, self.site.link("archive", y, lang)) + for y in years] context["permalink"] = self.site.link("archive", None, lang) task = self.site.generic_post_list_renderer( lang, diff --git a/nikola/plugins/task/build_less.plugin b/nikola/plugins/task/build_less.plugin deleted file mode 100644 index 27ca8cd..0000000 --- a/nikola/plugins/task/build_less.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index a672282..0000000 --- a/nikola/plugins/task/build_less.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 sys -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" - - def gen_tasks(self): - """Generate CSS out of LESS sources.""" - self.compiler_name = self.site.config['LESS_COMPILER'] - self.compiler_options = self.site.config['LESS_OPTIONS'] - - kw = { - 'cache_folder': self.site.config['CACHE_FOLDER'], - 'themes': self.site.THEMES, - } - tasks = {} - - # 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 - if os.path.isfile(os.path.join(self.sources_folder, "targets")): - targets_path = os.path.join(self.sources_folder, "targets") - else: - 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 task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): - if task['name'] in tasks: - continue - task['basename'] = 'prepare_less_sources' - tasks[task['name']] = task - yield task - - 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) - run_in_shell = sys.platform == 'win32' - try: - compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) - except OSError: - utils.req_missing([self.compiler_name], - 'build LESS files (and use this theme)', - False, False) - 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 deleted file mode 100644 index 746c1df..0000000 --- a/nikola/plugins/task/build_sass.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[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 deleted file mode 100644 index becc843..0000000 --- a/nikola/plugins/task/build_sass.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 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 sys -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") - - def gen_tasks(self): - """Generate CSS out of Sass sources.""" - self.logger = utils.get_logger('build_sass', self.site.loghandlers) - self.compiler_name = self.site.config['SASS_COMPILER'] - self.compiler_options = self.site.config['SASS_OPTIONS'] - - kw = { - 'cache_folder': self.site.config['CACHE_FOLDER'], - 'themes': self.site.THEMES, - } - tasks = {} - - # 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 - if os.path.isfile(os.path.join(self.sources_folder, "targets")): - targets_path = os.path.join(self.sources_folder, "targets") - else: - 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 task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): - if task['name'] in tasks: - continue - task['basename'] = 'prepare_sass_sources' - tasks[task['name']] = task - yield task - - 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)): - if task['name'] in tasks: - continue - task['basename'] = 'prepare_sass_sources' - tasks[task['name']] = task - 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) - run_in_shell = sys.platform == 'win32' - src = os.path.join(kw['cache_folder'], self.sources_folder, target) - try: - compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) - except OSError: - utils.req_missing([self.compiler_name], - 'build Sass files (and use this theme)', - False, False) - with open(dst, "wb+") as outf: - outf.write(compiled) - - yield self.group_task() - - # We can have file conflicts. This is a way to prevent them. - # I orignally wanted to use sets and their cannot-have-duplicates - # magic, but I decided not to do this so we can show the user - # what files were problematic. - # If we didn’t do this, there would be a cryptic message from doit - # instead. - seennames = {} - for target in targets: - base = os.path.splitext(target)[0] - dst = os.path.join(dst_dir, base + ".css") - - if base in seennames: - self.logger.error( - 'Duplicate filenames for Sass compiled files: {0} and ' - '{1} (both compile to {2})'.format( - seennames[base], target, base + ".css")) - else: - seennames.update({base: target}) - - yield { - 'basename': self.name, - 'name': dst, - 'targets': [dst], - 'file_dep': deps, - 'task_dep': ['prepare_sass_sources'], - 'actions': ((compile_target, [target, dst]), ), - 'uptodate': [utils.config_changed(kw)], - 'clean': True - } diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py index fcfaf42..7437a9d 100644 --- a/nikola/plugins/task/bundles.py +++ b/nikola/plugins/task/bundles.py @@ -65,8 +65,7 @@ class BuildBundles(LateTask): def build_bundle(output, inputs): out_dir = os.path.join(kw['output_folder'], os.path.dirname(output)) - inputs = [i for i in inputs if os.path.isfile( - os.path.join(out_dir, i))] + inputs = [os.path.relpath(i, out_dir) for i in inputs if os.path.isfile(i)] cache_dir = os.path.join(kw['cache_folder'], 'webassets') utils.makedirs(cache_dir) env = webassets.Environment(out_dir, os.path.dirname(output), @@ -83,20 +82,32 @@ class BuildBundles(LateTask): 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(): + for name, _files in kw['theme_bundles'].items(): output_path = os.path.join(kw['output_folder'], name) dname = os.path.dirname(name) - file_dep = [os.path.join(kw['output_folder'], dname, fname) + files = [] + for fname in _files: + # paths are relative to dirname + files.append(os.path.join(dname, fname)) + file_dep = [os.path.join(kw['output_folder'], fname) for fname in files if - utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS'])] + utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS']) + or fname == 'assets/css/code.css'] + # code.css will be generated by us if it does not exist in + # FILES_FOLDERS or theme assets. It is guaranteed that the + # generation will happen before this task. task = { 'file_dep': list(file_dep), - 'task_dep': ['copy_assets'], + 'task_dep': ['copy_assets', 'copy_files'], 'basename': str(self.name), 'name': str(output_path), - 'actions': [(build_bundle, (name, files))], + 'actions': [(build_bundle, (name, file_dep))], 'targets': [output_path], - 'uptodate': [utils.config_changed(kw)], + 'uptodate': [ + utils.config_changed({ + 1: kw, + 2: file_dep + })], 'clean': True, } yield utils.apply_filters(task, kw['filters']) diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py index 93b7fb3..4801347 100644 --- a/nikola/plugins/task/copy_assets.py +++ b/nikola/plugins/task/copy_assets.py @@ -45,13 +45,21 @@ class CopyAssets(Task): kw = { "themes": self.site.THEMES, + "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], + "code.css_selectors": 'pre.code', + "code.css_head": '/* code.css file generated by Nikola */\n', + "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", } - has_code_css = False tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') + code_css_input = utils.get_asset_path('assets/css/code.css', + themes=kw['themes'], + files_folders=kw['files_folders']) + + kw["code.css_input"] = code_css_input yield self.group_task() @@ -61,28 +69,35 @@ class CopyAssets(Task): for task in utils.copy_tree(src, dst): if task['name'] in tasks: continue - if task['targets'][0] == code_css_path: - has_code_css = True tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw)] task['basename'] = self.name + if code_css_input: + task['file_dep'] = [code_css_input] yield utils.apply_filters(task, kw['filters']) - if not has_code_css: # Generate it - + # Check whether or not there is a code.css file around. + if not code_css_input: 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(['pre.code', 'div.code pre'])) - outf.write("\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n") + outf.write(kw["code.css_head"]) + outf.write(formatter.get_style_defs(kw["code.css_selectors"])) + outf.write(kw["code.css_close"]) + + if os.path.exists(code_css_path): + with codecs.open(code_css_path, 'r', 'utf-8') as fh: + testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"] + else: + testcontents = False task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], - 'uptodate': [utils.config_changed(kw)], + 'uptodate': [utils.config_changed(kw), testcontents], 'actions': [(create_code_css, [])], 'clean': True, } diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index 880d47c..366374b 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -36,6 +36,7 @@ try: except ImportError: from urllib.parse import urljoin # NOQA +import natsort Image = None try: from PIL import Image, ExifTags # NOQA @@ -46,6 +47,7 @@ except ImportError: Image = _Image except ImportError: pass + import PyRSS2Gen as rss from nikola.plugin_categories import Task @@ -97,9 +99,15 @@ class Galleries(Task): 'filters': self.site.config['FILTERS'], 'translations': self.site.config['TRANSLATIONS'], 'global_context': self.site.GLOBAL_CONTEXT, - "feed_length": self.site.config['FEED_LENGTH'], + 'feed_length': self.site.config['FEED_LENGTH'], + 'tzinfo': self.site.tzinfo, + 'comments_in_galleries': self.site.config['COMMENTS_IN_GALLERIES'], + 'generate_rss': self.site.config['GENERATE_RSS'], } + for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): + self.kw['||template_hooks|{0}||'.format(k)] = v._items + yield self.group_task() template_name = "gallery.tmpl" @@ -152,6 +160,9 @@ class Galleries(Task): os.path.relpath(gallery, self.kw['gallery_path']), lang)) dst = os.path.normpath(dst) + for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: + self.kw[k] = self.site.GLOBAL_CONTEXT[k](lang) + context = {} context["lang"] = lang if post: @@ -165,12 +176,8 @@ class Galleries(Task): 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))) + name_without_ext = os.path.splitext(os.path.basename(fn))[0] + img_titles.append(utils.unslugify(name_without_ext)) else: img_titles = [''] * len(image_name_list) @@ -189,27 +196,30 @@ class Galleries(Task): ft = folder folders.append((folder, ft)) - ## TODO: in v7 remove images from context, use photo_array - context["images"] = list(zip(image_name_list, thumbs, img_titles)) - context["folders"] = folders + context["folders"] = natsort.natsorted(folders) context["crumbs"] = crumbs context["permalink"] = self.site.link( "gallery", os.path.basename( os.path.relpath(gallery, self.kw['gallery_path'])), lang) - # FIXME: use kw - context["enable_comments"] = ( - self.site.config["COMMENTS_IN_GALLERIES"]) + context["enable_comments"] = self.kw['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) + yield { + 'basename': self.name, + 'name': post.translated_base_path(lang), + 'targets': [post.translated_base_path(lang)], + 'file_dep': post.fragment_deps(lang), + 'actions': [(post.compile, [lang])], + 'uptodate': [utils.config_changed(self.kw)] + } + context['post'] = post else: - context['text'] = '' - + context['post'] = None file_dep = self.site.template_system.template_deps( template_name) + image_list + thumbs + if post: + file_dep += [post.translated_base_path(l) for l in self.kw['translations']] yield utils.apply_filters({ 'basename': self.name, @@ -222,6 +232,7 @@ class Galleries(Task): dst, context, dest_img_list, + img_titles, thumbs, file_dep))], 'clean': True, @@ -233,39 +244,40 @@ class Galleries(Task): }, 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']) + if self.kw["generate_rss"]: + 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']): + for root, dirs, files in os.walk(self.kw['gallery_path'], followlinks=True): self.gallery_list.append(root) def create_galleries(self): @@ -433,6 +445,7 @@ class Galleries(Task): output_name, context, img_list, + img_titles, thumbs, file_dep): """Build the gallery index.""" @@ -446,12 +459,9 @@ class Galleries(Task): return url photo_array = [] - for img, thumb in zip(img_list, thumbs): + for img, thumb, title in zip(img_list, thumbs, img_titles): 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), @@ -462,9 +472,8 @@ class Galleries(Task): 'h': h }, }) - context['photo_array_json'] = json.dumps(photo_array) context['photo_array'] = photo_array - + context['photo_array_json'] = json.dumps(photo_array) self.site.render_template(template_name, output_name, context) def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title): @@ -478,12 +487,12 @@ class Galleries(Task): return urljoin(self.site.config['BASE_URL'], url) items = [] - for img, full_title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]: + for img, 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] if full_title else '', + 'title': title, 'link': make_url(img), 'guid': rss.Guid(img, False), 'pubDate': self.image_date(img), @@ -494,17 +503,16 @@ class Galleries(Task): ), } items.append(rss.RSSItem(**args)) - rss_obj = utils.ExtendedRSS2( + rss_obj = rss.RSS2( title=title, link=make_url(permalink), description='', lastBuildDate=datetime.datetime.now(), items=items, - generator='Nikola <http://getnikola.com/>', + generator='http://getnikola.com/', language=lang ) - rss_obj.self_url = make_url(permalink) - rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" + rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" dst_dir = os.path.dirname(output_path) utils.makedirs(dst_dir) with codecs.open(output_path, "wb+", "utf-8") as rss_file: @@ -564,7 +572,7 @@ class Galleries(Task): if exif is not None: for tag, value in list(exif.items()): decoded = ExifTags.TAGS.get(tag, tag) - if decoded == 'DateTimeOriginal': + if decoded in ('DateTimeOriginal', 'DateTimeDigitized'): try: self.dates[src] = datetime.datetime.strptime( value, r'%Y:%m:%d %H:%M:%S') diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index 3f45161..386cc18 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -25,8 +25,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals -import glob -import itertools +from collections import defaultdict import os from nikola.plugin_categories import Task @@ -54,22 +53,23 @@ class Indexes(Task): "index_teasers": self.site.config['INDEX_TEASERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], - "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], "indexes_title": self.site.config['INDEXES_TITLE'], "indexes_pages": self.site.config['INDEXES_PAGES'], "indexes_pages_main": self.site.config['INDEXES_PAGES_MAIN'], "blog_title": self.site.config["BLOG_TITLE"], + "rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"], } template_name = "index.tmpl" - posts = [x for x in self.site.timeline if x.use_in_feeds] + posts = self.site.posts for lang in kw["translations"]: # Split in smaller lists lists = [] - if kw["hide_untranslated_posts"]: - filtered_posts = [x for x in posts if x.is_translation_available(lang)] - else: + if kw["show_untranslated_posts"]: filtered_posts = posts + else: + filtered_posts = [x for x in posts if x.is_translation_available(lang)] lists.append(filtered_posts[:kw["index_display_post_count"]]) filtered_posts = filtered_posts[kw["index_display_post_count"]:] while filtered_posts: @@ -78,7 +78,7 @@ class Indexes(Task): num_pages = len(lists) for i, post_list in enumerate(lists): context = {} - indexes_title = kw['indexes_title'] or kw['blog_title'] + indexes_title = kw['indexes_title'] or kw['blog_title'](lang) if kw["indexes_pages_main"]: ipages_i = i + 1 ipages_msg = "page %d" @@ -134,33 +134,33 @@ class Indexes(Task): "post_pages": self.site.config["post_pages"], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], + "index_file": self.site.config['INDEX_FILE'], } template_name = "list.tmpl" for lang in kw["translations"]: # Need to group by folder to avoid duplicated tasks (Issue #758) - for dirname, wildcards in itertools.groupby((w for w, d, x, i in kw["post_pages"] if not i), os.path.dirname): - context = {} - # vim/pyflakes thinks it's unused - # src_dir = os.path.dirname(wildcard) - files = [] - for wildcard in wildcards: - files += glob.glob(wildcard) - post_list = [self.site.global_data[p] for p in files] - output_name = os.path.join(kw["output_folder"], - self.site.path("post_path", - wildcard, - lang)).encode('utf8') - context["items"] = [(post.title(lang), post.permalink(lang)) - for post in post_list] - task = self.site.generic_post_list_renderer(lang, post_list, - output_name, - template_name, - kw['filters'], - context) - task_cfg = {1: task['uptodate'][0].config, 2: kw} - task['uptodate'] = [config_changed(task_cfg)] - task['basename'] = self.name - yield task + # Group all pages by path prefix + groups = defaultdict(list) + for p in self.site.timeline: + if not p.is_post: + dirname = os.path.dirname(p.destination_path(lang)) + groups[dirname].append(p) + for dirname, post_list in groups.items(): + context = {} + context["items"] = [ + (post.title(lang), post.permalink(lang)) + for post in post_list + ] + output_name = os.path.join(kw['output_folder'], dirname, kw['index_file']) + task = self.site.generic_post_list_renderer(lang, post_list, + output_name, + template_name, + kw['filters'], + context) + task_cfg = {1: task['uptodate'][0].config, 2: kw} + task['uptodate'] = [config_changed(task_cfg)] + task['basename'] = self.name + yield task def index_path(self, name, lang): if name not in [None, 0]: diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py index 86be6c4..a0fe974 100644 --- a/nikola/plugins/task/listings.py +++ b/nikola/plugins/task/listings.py @@ -31,11 +31,17 @@ import os from pygments import highlight from pygments.lexers import get_lexer_for_filename, TextLexer from pygments.formatters import HtmlFormatter +import natsort +import re from nikola.plugin_categories import Task from nikola import utils +# FIXME: (almost) duplicated with mdx_nikola.py +CODERE = re.compile('<div class="code"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL) + + class Listings(Task): """Render pretty listings.""" @@ -69,6 +75,9 @@ class Listings(Task): linenos="table", nowrap=False, lineanchors=utils.slugify(in_name), anchorlinenos=True)) + # the pygments highlighter uses <div class="codehilite"><pre> + # for code. We switch it to reST's <pre class="code">. + code = CODERE.sub('<pre class="code literal-block">\\1</pre>', code) title = os.path.basename(in_name) else: code = '' @@ -76,14 +85,27 @@ class Listings(Task): crumbs = utils.get_crumbs(os.path.relpath(out_name, kw['output_folder']), is_file=True) + permalink = self.site.link( + 'listing', + os.path.relpath( + out_name, + os.path.join( + kw['output_folder'], + kw['listings_folder']))) + if self.site.config['COPY_SOURCES']: + source_link = permalink[:-5] + else: + source_link = None context = { 'code': code, 'title': title, 'crumbs': crumbs, + 'permalink': permalink, 'lang': kw['default_lang'], - 'folders': folders, - 'files': files, + 'folders': natsort.natsorted(folders), + 'files': natsort.natsorted(files), 'description': title, + 'source_link': source_link, } self.site.render_template('listing.tmpl', out_name, context) @@ -91,7 +113,21 @@ class Listings(Task): yield self.group_task() template_deps = self.site.template_system.template_deps('listing.tmpl') - for root, dirs, files in os.walk(kw['listings_folder']): + for root, dirs, files in os.walk(kw['listings_folder'], followlinks=True): + files = [f for f in files if os.path.splitext(f)[-1] not in ignored_extensions] + + uptodate = {'c': self.site.GLOBAL_CONTEXT} + + for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): + uptodate['||template_hooks|{0}||'.format(k)] = v._items + + for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: + uptodate[k] = self.site.GLOBAL_CONTEXT[k](kw['default_lang']) + + uptodate2 = uptodate.copy() + uptodate2['f'] = files + uptodate2['d'] = dirs + # Render all files out_name = os.path.join( kw['output_folder'], @@ -105,8 +141,7 @@ class Listings(Task): 'actions': [(render_listing, [None, out_name, dirs, files])], # This is necessary to reflect changes in blog title, # sidebar links, etc. - 'uptodate': [utils.config_changed( - self.site.GLOBAL_CONTEXT)], + 'uptodate': [utils.config_changed(uptodate2)], 'clean': True, } for f in files: @@ -126,11 +161,25 @@ class Listings(Task): 'actions': [(render_listing, [in_name, out_name])], # This is necessary to reflect changes in blog title, # sidebar links, etc. - 'uptodate': [utils.config_changed( - self.site.GLOBAL_CONTEXT)], + 'uptodate': [utils.config_changed(uptodate)], 'clean': True, } + if self.site.config['COPY_SOURCES']: + out_name = os.path.join( + kw['output_folder'], + root, + f) + yield { + 'basename': self.name, + 'name': out_name, + 'file_dep': [in_name], + 'targets': [out_name], + 'actions': [(utils.copy_file, [in_name, out_name])], + 'clean': True, + } def listing_path(self, name, lang): - return [_f for _f in [self.site.config['LISTINGS_FOLDER'], name + - '.html'] if _f] + if not name.endswith('.html'): + name += '.html' + path_parts = [self.site.config['LISTINGS_FOLDER']] + list(os.path.split(name)) + return [_f for _f in path_parts if _f] diff --git a/nikola/plugins/task/localsearch.plugin b/nikola/plugins/task/localsearch.plugin deleted file mode 100644 index 86accb6..0000000 --- a/nikola/plugins/task/localsearch.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = local_search -Module = localsearch - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Create data files for local search via Tipue - diff --git a/nikola/plugins/task/localsearch/MIT-LICENSE.txt b/nikola/plugins/task/localsearch/MIT-LICENSE.txt deleted file mode 100644 index f131068..0000000 --- a/nikola/plugins/task/localsearch/MIT-LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Tipue Search Copyright (c) 2012 Tipue - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/nikola/plugins/task/localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py deleted file mode 100644 index c501d80..0000000 --- a/nikola/plugins/task/localsearch/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import unicode_literals -import codecs -import json -import os - -from doit.tools import result_dep - -from nikola.plugin_categories import LateTask -from nikola.utils import config_changed, copy_tree, makedirs - -# This is what we need to produce: -#var tipuesearch = {"pages": [ - #{"title": "Tipue Search, a jQuery site search engine", "text": "Tipue - #Search is a site search engine jQuery plugin. It's free for both commercial and - #non-commercial use and released under the MIT License. Tipue Search includes - #features such as word stemming and word replacement.", "tags": "JavaScript", - #"loc": "http://www.tipue.com/search"}, - #{"title": "Tipue Search demo", "text": "Tipue Search demo. Tipue Search is - #a site search engine jQuery plugin.", "tags": "JavaScript", "loc": - #"http://www.tipue.com/search/demo"}, - #{"title": "About Tipue", "text": "Tipue is a small web development/design - #studio based in North London. We've been around for over a decade.", "tags": "", - #"loc": "http://www.tipue.com/about"} -#]}; - - -class Tipue(LateTask): - """Render the blog posts as JSON data.""" - - name = "local_search" - - def gen_tasks(self): - self.site.scan_posts() - - kw = { - "translations": self.site.config['TRANSLATIONS'], - "output_folder": self.site.config['OUTPUT_FOLDER'], - } - - posts = self.site.timeline[:] - dst_path = os.path.join(kw["output_folder"], "assets", "js", - "tipuesearch_content.json") - - def save_data(): - pages = [] - for lang in kw["translations"]: - for post in posts: - # Don't index drafts (Issue #387) - if post.is_draft or post.is_retired or post.publish_later: - continue - text = post.text(lang, strip_html=True) - text = text.replace('^', '') - - data = {} - data["title"] = post.title(lang) - data["text"] = text - data["tags"] = ",".join(post.tags) - data["loc"] = post.permalink(lang) - pages.append(data) - output = json.dumps({"pages": pages}, indent=2) - makedirs(os.path.dirname(dst_path)) - with codecs.open(dst_path, "wb+", "utf8") as fd: - fd.write(output) - - yield { - "basename": str(self.name), - "name": dst_path, - "targets": [dst_path], - "actions": [(save_data, [])], - 'uptodate': [config_changed(kw), result_dep('sitemap')] - } - # Note: The task should run everytime a new file is added or a - # file is changed. We cheat, and depend on the sitemap task, - # to run everytime a new file is added. - - # Copy all the assets to the right places - asset_folder = os.path.join(os.path.dirname(__file__), "files") - for task in copy_tree(asset_folder, kw["output_folder"]): - task["basename"] = str(self.name) - yield task diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif Binary files differdeleted file mode 100644 index 9c97738..0000000 --- a/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif +++ /dev/null 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 differdeleted file mode 100644 index 9ab0f2c..0000000 --- a/nikola/plugins/task/localsearch/files/assets/css/img/search.png +++ /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 100644 index 2230193..0000000 --- a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css +++ /dev/null @@ -1,159 +0,0 @@ - -/* -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 deleted file mode 100644 index a9982cd..0000000 --- a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js +++ /dev/null @@ -1,384 +0,0 @@ - -/* -Tipue Search 3.0.1 -Copyright (c) 2013 Tipue -Tipue Search is released under the MIT License -http://www.tipue.com/search -*/ - - -(function($) { - - $.fn.tipuesearch = function(options) { - - var set = $.extend( { - - 'show' : 7, - 'newWindow' : false, - 'showURL' : true, - 'minimumLength' : 3, - 'descriptiveWords' : 25, - 'highlightTerms' : true, - 'highlightEveryTerm' : false, - 'mode' : 'static', - 'liveDescription' : '*', - 'liveContent' : '*', - 'contentLocation' : 'tipuesearch/tipuesearch_content.json' - - }, options); - - return this.each(function() { - - var tipuesearch_in = { - pages: [] - }; - $.ajaxSetup({ - async: false - }); - - if (set.mode == 'live') - { - for (var i = 0; i < tipuesearch_pages.length; i++) - { - $.get(tipuesearch_pages[i], '', - function (html) - { - var cont = $(set.liveContent, html).text(); - cont = cont.replace(/\s+/g, ' '); - var desc = $(set.liveDescription, html).text(); - desc = desc.replace(/\s+/g, ' '); - - var t_1 = html.toLowerCase().indexOf('<title>'); - var t_2 = html.toLowerCase().indexOf('</title>', t_1 + 7); - if (t_1 != -1 && t_2 != -1) - { - var tit = html.slice(t_1 + 7, t_2); - } - else - { - var tit = 'No title'; - } - - tipuesearch_in.pages.push({ - "title": tit, - "text": desc, - "tags": cont, - "loc": tipuesearch_pages[i] - }); - } - ); - } - } - - if (set.mode == 'json') - { - $.getJSON(set.contentLocation, - function(json) - { - tipuesearch_in = $.extend({}, json); - } - ); - } - - if (set.mode == 'static') - { - tipuesearch_in = $.extend({}, tipuesearch); - } - - var tipue_search_w = ''; - if (set.newWindow) - { - tipue_search_w = ' target="_blank"'; - } - - function getURLP(name) - { - return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null; - } - if (getURLP('q')) - { - $('#tipue_search_input').val(getURLP('q')); - getTipueSearch(0, true); - } - - $('#tipue_search_button').click(function() - { - getTipueSearch(0, true); - }); - $(this).keyup(function(event) - { - if(event.keyCode == '13') - { - getTipueSearch(0, true); - } - }); - - function getTipueSearch(start, replace) - { - $('#tipue_search_content').hide(); - var out = ''; - var results = ''; - var show_replace = false; - var show_stop = false; - - var d = $('#tipue_search_input').val().toLowerCase(); - d = $.trim(d); - var d_w = d.split(' '); - d = ''; - for (var i = 0; i < d_w.length; i++) - { - var a_w = true; - for (var f = 0; f < tipuesearch_stop_words.length; f++) - { - if (d_w[i] == tipuesearch_stop_words[f]) - { - a_w = false; - show_stop = true; - } - } - if (a_w) - { - d = d + ' ' + d_w[i]; - } - } - d = $.trim(d); - d_w = d.split(' '); - - if (d.length >= set.minimumLength) - { - if (replace) - { - var d_r = d; - for (var i = 0; i < d_w.length; i++) - { - for (var f = 0; f < tipuesearch_replace.words.length; f++) - { - if (d_w[i] == tipuesearch_replace.words[f].word) - { - d = d.replace(d_w[i], tipuesearch_replace.words[f].replace_with); - show_replace = true; - } - } - } - d_w = d.split(' '); - } - - var d_t = d; - for (var i = 0; i < d_w.length; i++) - { - for (var f = 0; f < tipuesearch_stem.words.length; f++) - { - if (d_w[i] == tipuesearch_stem.words[f].word) - { - d_t = d_t + ' ' + tipuesearch_stem.words[f].stem; - } - } - } - d_w = d_t.split(' '); - - var c = 0; - found = new Array(); - for (var i = 0; i < tipuesearch_in.pages.length; i++) - { - var score = 1000000000; - var s_t = tipuesearch_in.pages[i].text; - for (var f = 0; f < d_w.length; f++) - { - var pat = new RegExp(d_w[f], 'i'); - if (tipuesearch_in.pages[i].title.search(pat) != -1) - { - score -= (200000 - i); - } - if (tipuesearch_in.pages[i].text.search(pat) != -1) - { - score -= (150000 - i); - } - - if (set.highlightTerms) - { - if (set.highlightEveryTerm) - { - var patr = new RegExp('(' + d_w[f] + ')', 'gi'); - } - else - { - var patr = new RegExp('(' + d_w[f] + ')', 'i'); - } - s_t = s_t.replace(patr, "<b>$1</b>"); - } - if (tipuesearch_in.pages[i].tags.search(pat) != -1) - { - score -= (100000 - i); - } - - } - if (score < 1000000000) - { - found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc; - } - } - - if (c != 0) - { - if (show_replace == 1) - { - out += '<div id="tipue_search_warning_head">Showing results for ' + d + '</div>'; - out += '<div id="tipue_search_warning">Search for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>'; - } - if (c == 1) - { - out += '<div id="tipue_search_results_count">1 result</div>'; - } - else - { - c_c = c.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); - out += '<div id="tipue_search_results_count">' + c_c + ' results</div>'; - } - - found.sort(); - var l_o = 0; - for (var i = 0; i < found.length; i++) - { - var fo = found[i].split('^'); - if (l_o >= start && l_o < set.show + start) - { - out += '<div class="tipue_search_content_title"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[1] + '</a></div>'; - - var t = fo[2]; - var t_d = ''; - var t_w = t.split(' '); - if (t_w.length < set.descriptiveWords) - { - t_d = t; - } - else - { - for (var f = 0; f < set.descriptiveWords; f++) - { - t_d += t_w[f] + ' '; - } - } - t_d = $.trim(t_d); - if (t_d.charAt(t_d.length - 1) != '.') - { - t_d += ' ...'; - } - out += '<div class="tipue_search_content_text">' + t_d + '</div>'; - - if (set.showURL) - { - t_url = fo[3]; - if (t_url.length > 45) - { - t_url = fo[3].substr(0, 45) + ' ...'; - } - out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + t_url + '</a></div>'; - } - } - l_o++; - } - - if (c > set.show) - { - var pages = Math.ceil(c / set.show); - var page = (start / set.show); - out += '<div id="tipue_search_foot"><ul id="tipue_search_foot_boxes">'; - - if (start > 0) - { - out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">Prev</a></li>'; - } - - if (page <= 2) - { - var p_b = pages; - if (pages > 3) - { - p_b = 3; - } - for (var f = 0; f < p_b; f++) - { - if (f == page) - { - out += '<li class="current">' + (f + 1) + '</li>'; - } - else - { - out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>'; - } - } - } - else - { - var p_b = page + 3; - if (p_b > pages) - { - p_b = pages; - } - for (var f = page; f < p_b; f++) - { - if (f == page) - { - out += '<li class="current">' + (f + 1) + '</li>'; - } - else - { - out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>'; - } - } - } - - if (page + 1 != pages) - { - out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next</a></li>'; - } - - out += '</ul></div>'; - } - } - else - { - out += '<div id="tipue_search_warning_head">Nothing found</div>'; - } - } - else - { - if (show_stop) - { - out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>'; - } - else - { - out += '<div id="tipue_search_warning_head">Search too short</div>'; - if (set.minimumLength == 1) - { - out += '<div id="tipue_search_warning">Should be one character or more</div>'; - } - else - { - out += '<div id="tipue_search_warning">Should be ' + set.minimumLength + ' characters or more</div>'; - } - } - } - - $('#tipue_search_content').html(out); - $('#tipue_search_content').slideDown(200); - - $('#tipue_search_replaced').click(function() - { - getTipueSearch(0, false); - }); - - $('.tipue_search_foot_box').click(function() - { - var id_v = $(this).attr('id'); - var id_a = id_v.split('_'); - - getTipueSearch(parseInt(id_a[0]), id_a[1]); - }); - } - - }); - }; - -})(jQuery); diff --git a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js deleted file mode 100644 index 8493ec1..0000000 --- a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js +++ /dev/null @@ -1,21 +0,0 @@ - -/* -Tipue Search 3.0.1 -Copyright (c) 2013 Tipue -Tipue Search is released under the MIT License -http://www.tipue.com/search -*/ - - -var tipuesearch_stop_words = ["and", "be", "by", "do", "for", "he", "how", "if", "is", "it", "my", "not", "of", "or", "the", "to", "up", "what", "when"]; - -var tipuesearch_replace = {"words": [ - {"word": "tipua", replace_with: "tipue"}, - {"word": "javscript", replace_with: "javascript"} -]}; - -var tipuesearch_stem = {"words": [ - {"word": "e-mail", stem: "email"}, - {"word": "javascript", stem: "script"}, - {"word": "javascript", stem: "js"} -]}; diff --git a/nikola/plugins/task/localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html deleted file mode 100644 index 789fbe5..0000000 --- a/nikola/plugins/task/localsearch/files/tipue_search.html +++ /dev/null @@ -1,31 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
-<html>
-<head>
-<title>Tipue Search</title>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
-
-<link rel="stylesheet" type="text/css" href="assets/css/tipuesearch.css">
-<script type="text/javascript" src="assets/js/tipuesearch_set.js"></script>
-<script type="text/javascript" src="assets/js/tipuesearch.js"></script>
-
-</head>
-<body>
-
-<div style="float: left;"><input type="text" id="tipue_search_input"></div>
-<div style="float: left; margin-left: 13px;"><input type="button" id="tipue_search_button"></div>
-<div id="tipue_search_content"><div id="tipue_search_loading"></div></div>
-</div>
-
-<script type="text/javascript">
-$(document).ready(function() {
- $('#tipue_search_input').tipuesearch({
- 'mode': 'json',
- 'contentLocation': 'assets/js/tipuesearch_content.json'
- });
-});
-</script>
-</body>
-</html>
diff --git a/nikola/plugins/task/mustache.plugin b/nikola/plugins/task/mustache.plugin deleted file mode 100644 index d6b487a..0000000 --- a/nikola/plugins/task/mustache.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = render_mustache -Module = mustache - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Generates the blog's index pages in json. - diff --git a/nikola/plugins/task/mustache/__init__.py b/nikola/plugins/task/mustache/__init__.py deleted file mode 100644 index 5be98f0..0000000 --- a/nikola/plugins/task/mustache/__init__.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2014 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import unicode_literals - -import codecs -import json -import os - -from nikola.plugin_categories import Task -from nikola.utils import ( - config_changed, copy_file, LocaleBorg, makedirs, unicode_str, -) - - -class Mustache(Task): - """Render the blog posts as JSON data.""" - - name = "render_mustache" - - def gen_tasks(self): - self.site.scan_posts() - - kw = { - "translations": self.site.config['TRANSLATIONS'], - "index_display_post_count": - self.site.config['INDEX_DISPLAY_POST_COUNT'], - "messages": self.site.MESSAGES, - "index_teasers": self.site.config['INDEX_TEASERS'], - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - "blog_title": self.site.config['BLOG_TITLE'], - "content_footer": self.site.config['CONTENT_FOOTER'], - } - - # TODO: timeline is global, get rid of it - posts = [x for x in self.site.timeline if x.use_in_feeds] - if not posts: - yield { - 'basename': 'render_mustache', - 'actions': [], - } - return - - def write_file(path, post, lang): - - # Prev/Next links - prev_link = False - if post.prev_post: - prev_link = post.prev_post.permalink(lang).replace(".html", - ".json") - next_link = False - if post.next_post: - next_link = post.next_post.permalink(lang).replace(".html", - ".json") - data = {} - - # Configuration - for k, v in self.site.config.items(): - if isinstance(v, (str, unicode_str)): # NOQA - data[k] = v - - # Tag data - tags = [] - for tag in post.tags: - tags.append({'name': tag, 'link': self.site.link("tag", tag, - lang)}) - data.update({ - "tags": tags, - "tags?": True if tags else False, - }) - - # Template strings - for k, v in kw["messages"][lang].items(): - data["message_" + k] = v - - # Post data - data.update({ - "title": post.title(lang), - "text": post.text(lang), - "prev": prev_link, - "next": next_link, - "date": - post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']), - }) - - # Comments - context = dict(post=post, lang=LocaleBorg().current_lang) - context.update(self.site.GLOBAL_CONTEXT) - data["comment_html"] = self.site.template_system.render_template( - 'mustache-comment-form.tmpl', None, context).strip() - - # Post translations - translations = [] - for langname in kw["translations"]: - if langname == lang: - continue - translations.append({'name': - kw["messages"][langname]["Read in English"], - 'link': "javascript:load_data('%s');" - % post.permalink(langname).replace( - ".html", ".json")}) - data["translations"] = translations - - makedirs(os.path.dirname(path)) - with codecs.open(path, 'wb+', 'utf8') as fd: - fd.write(json.dumps(data)) - - for lang in kw["translations"]: - for i, post in enumerate(posts): - out_path = post.destination_path(lang, ".json") - out_file = os.path.join(kw['output_folder'], out_path) - task = { - 'basename': 'render_mustache', - 'name': out_file, - 'file_dep': post.fragment_deps(lang), - 'targets': [out_file], - 'actions': [(write_file, (out_file, post, lang))], - 'task_dep': ['render_posts'], - 'uptodate': [config_changed({ - 1: post.text(lang), - 2: post.prev_post, - 3: post.next_post, - 4: post.title(lang), - })] - } - yield task - - if posts: - first_post_data = posts[0].permalink( - self.site.config["DEFAULT_LANG"]).replace(".html", ".json") - - # Copy mustache template - src = os.path.join(os.path.dirname(__file__), 'mustache-template.html') - dst = os.path.join(kw['output_folder'], 'mustache-template.html') - yield { - 'basename': 'render_mustache', - 'name': dst, - 'targets': [dst], - 'file_dep': [src], - 'actions': [(copy_file, (src, dst))], - } - - # Copy mustache.html with the right starting file in it - src = os.path.join(os.path.dirname(__file__), 'mustache.html') - dst = os.path.join(kw['output_folder'], 'mustache.html') - - def copy_mustache(): - with codecs.open(src, 'rb', 'utf8') as in_file: - with codecs.open(dst, 'wb+', 'utf8') as out_file: - data = in_file.read().replace('{{first_post_data}}', - first_post_data) - out_file.write(data) - yield { - 'basename': 'render_mustache', - 'name': dst, - 'targets': [dst], - 'file_dep': [src], - 'uptodate': [config_changed({1: first_post_data})], - 'actions': [(copy_mustache, [])], - } diff --git a/nikola/plugins/task/mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html deleted file mode 100644 index e9a0213..0000000 --- a/nikola/plugins/task/mustache/mustache-template.html +++ /dev/null @@ -1,29 +0,0 @@ -<script id="view" type="text/html"> -<div class="container" id="container"> - <div class="postbox"> - <h1>{{BLOG_TITLE}}</h1> - <hr> - <h2>{{title}}</h2> - Posted on: {{date}}</br> - {{#tags?}} More posts about: - {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}} - </br> - {{/tags?}} - {{#translations}}<a href={{link}}>{{name}}</a>{{/translations}} </br> - <hr> - {{{text}}} - <ul class="pager"> - {{#prev}} - <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li> - {{/prev}} - {{#next}} - <li class="next"><a href="javascript:load_data('{{next}}')">{{message_Next post}}</a></li> - {{/next}} - </ul> - {{{comment_html}}} - </div> - <div class="footerbox"> - {{{CONTENT_FOOTER}}} - </div> -</div> -</script> diff --git a/nikola/plugins/task/mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html deleted file mode 100644 index 7ff6312..0000000 --- a/nikola/plugins/task/mustache/mustache.html +++ /dev/null @@ -1,34 +0,0 @@ -<head> - <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> - <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> - <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> - <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script> - <script src="//cdn.jsdelivr.net/jquery.mustache/0.2.7/jquery.mustache.js"></script> - <script src="//cdn.jsdelivr.net/mustache.js/0.7.2/mustache.js"></script> - <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> - <script type="text/javascript"> -function load_data(dataurl) { - jQuery.getJSON(dataurl, function(data) { - $('body').mustache('view', data, { method: 'html' }); - window.location.hash = '#' + dataurl; - }) -}; -$(document).ready(function() { -$.Mustache.load('/mustache-template.html') - .done(function () { - if (window.location.hash != '') { - load_data(window.location.hash.slice(1)); - } - else { - load_data('{{first_post_data}}'); - }; - }) -}); -</script> -</head> -<body style="padding-top: 0;"> -</body> diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py index f4c0469..aefc5a1 100644 --- a/nikola/plugins/task/pages.py +++ b/nikola/plugins/task/pages.py @@ -40,13 +40,14 @@ class RenderPages(Task): "post_pages": self.site.config["post_pages"], "translations": self.site.config["TRANSLATIONS"], "filters": self.site.config["FILTERS"], - "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], + "demote_headers": self.site.config['DEMOTE_HEADERS'], } self.site.scan_posts() yield self.group_task() for lang in kw["translations"]: for post in self.site.timeline: - if kw["hide_untranslated_posts"] and not post.is_translation_available(lang): + if not kw["show_untranslated_posts"] and not post.is_translation_available(lang): continue for task in self.site.generic_page_renderer(lang, post, kw["filters"]): diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py index a502b81..8e03122 100644 --- a/nikola/plugins/task/posts.py +++ b/nikola/plugins/task/posts.py @@ -25,12 +25,20 @@ # 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 +def rest_deps(post, task): + """Add extra_deps from ReST into task. + + The .dep file is created by ReST so not available before the task starts + to execute. + """ + task.file_dep.update(post.extra_deps()) + + class RenderPosts(Task): """Build HTML fragments from metadata and text.""" @@ -43,10 +51,10 @@ class RenderPosts(Task): "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'], + "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], + "demote_headers": self.site.config['DEMOTE_HEADERS'], } - nikola.post.READ_MORE_LINK = self.site.config['READ_MORE_LINK'] yield self.group_task() for lang in kw["translations"]: @@ -59,7 +67,9 @@ class RenderPosts(Task): 'name': dest, 'file_dep': post.fragment_deps(lang), 'targets': [dest], - 'actions': [(post.compile, (lang, ))], + 'actions': [(post.compile, (lang, )), + (rest_deps, (post,)), + ], 'clean': True, 'uptodate': [utils.config_changed(deps_dict)], } diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py index 6fafd13..eccc0ab 100644 --- a/nikola/plugins/task/redirect.py +++ b/nikola/plugins/task/redirect.py @@ -63,4 +63,4 @@ def create_redirect(src, dst): with codecs.open(src, "wb+", "utf8") as fd: fd.write('<!DOCTYPE html><head><title>Redirecting...</title>' '<meta http-equiv="refresh" content="0; ' - 'url={0}"></head>'.format(dst)) + 'url={0}"></head><body><p>Page moved <a href="{0}">here</a></p></body>'.format(dst)) diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin new file mode 100644 index 0000000..60b50fb --- /dev/null +++ b/nikola/plugins/task/robots.plugin @@ -0,0 +1,10 @@ +[Core] +Name = robots +Module = robots + +[Documentation] +Author = Daniel Aleksandersen +Version = 0.1 +Website = http://getnikola.com +Description = Generate /robots.txt exclusion file and promote sitemap. + diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py new file mode 100644 index 0000000..9944c0d --- /dev/null +++ b/nikola/plugins/task/robots.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2014 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 os +try: + from urlparse import urljoin, urlparse +except ImportError: + from urllib.parse import urljoin, urlparse # NOQA + +from nikola.plugin_categories import LateTask +from nikola import utils + + +class RobotsFile(LateTask): + """Generate a robots.txt.""" + + name = "robots_file" + + def gen_tasks(self): + """Generate a robots.txt.""" + kw = { + "base_url": self.site.config["BASE_URL"], + "site_url": self.site.config["SITE_URL"], + "output_folder": self.site.config["OUTPUT_FOLDER"], + "files_folders": self.site.config['FILES_FOLDERS'], + "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"] + } + + if kw["site_url"] != urljoin(kw["site_url"], "/"): + utils.LOGGER.warn('robots.txt not ending up in server root, will be useless') + + sitemapindex_url = urljoin(kw["base_url"], "sitemapindex.xml") + robots_path = os.path.join(kw['output_folder'], "robots.txt") + + def write_robots(): + with codecs.open(robots_path, 'wb+', 'utf8') as outf: + outf.write("Sitemap: {0}\n\n".format(sitemapindex_url)) + if kw["robots_exclusions"]: + outf.write("User-Agent: *\n") + for loc in kw["robots_exclusions"]: + outf.write("Disallow: {0}\n".format(loc)) + + yield self.group_task() + + if not utils.get_asset_path("robots.txt", [], files_folders=kw["files_folders"]): + yield { + "basename": self.name, + "name": robots_path, + "targets": [robots_path], + "actions": [(write_robots)], + "uptodate": [utils.config_changed(kw)], + "clean": True, + "task_dep": ["sitemap"] + } + elif kw["robots_exclusions"]: + utils.LOGGER.warn('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied fie.') + else: + utils.LOGGER.debug('Did not generate robots.txt as one already exists in FILES_FOLDERS.') diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py index 9e4204c..b16ed48 100644 --- a/nikola/plugins/task/rss.py +++ b/nikola/plugins/task/rss.py @@ -54,8 +54,11 @@ class GenerateRSS(Task): "blog_description": self.site.config["BLOG_DESCRIPTION"], "output_folder": self.site.config["OUTPUT_FOLDER"], "rss_teasers": self.site.config["RSS_TEASERS"], - "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "rss_plain": self.site.config["RSS_PLAIN"], + "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], "feed_length": self.site.config['FEED_LENGTH'], + "tzinfo": self.site.tzinfo, + "rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"], } self.site.scan_posts() # Check for any changes in the state of use_in_feeds for any post. @@ -68,24 +71,25 @@ class GenerateRSS(Task): output_name = os.path.join(kw['output_folder'], self.site.path("rss", None, lang)) deps = [] - if kw["hide_untranslated_posts"]: - posts = [x for x in self.site.timeline if x.use_in_feeds - and x.is_translation_available(lang)][:10] + if kw["show_untranslated_posts"]: + posts = self.site.posts[:10] else: - posts = [x for x in self.site.timeline if x.use_in_feeds][:10] + posts = [x for x in self.site.posts if x.is_translation_available(lang)][:10] for post in posts: deps += post.deps(lang) feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/')) + yield { 'basename': 'generate_rss', 'name': os.path.normpath(output_name), 'file_dep': deps, 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"], kw["site_url"], - kw["blog_description"], posts, output_name, - kw["rss_teasers"], kw['feed_length'], feed_url))], + (lang, kw["blog_title"](lang), kw["site_url"], + kw["blog_description"](lang), posts, output_name, + kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], feed_url))], + 'task_dep': ['render_posts'], 'clean': True, 'uptodate': [utils.config_changed(kw)], diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py index 147bd50..beac6cb 100644 --- a/nikola/plugins/task/sitemap/__init__.py +++ b/nikola/plugins/task/sitemap/__init__.py @@ -30,14 +30,16 @@ import datetime import os try: from urlparse import urljoin, urlparse + import robotparser as robotparser except ImportError: from urllib.parse import urljoin, urlparse # NOQA + import urllib.robotparser as robotparser # NOQA from nikola.plugin_categories import LateTask from nikola.utils import config_changed -header = """<?xml version="1.0" encoding="UTF-8"?> +urlset_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" @@ -45,13 +47,29 @@ header = """<?xml version="1.0" encoding="UTF-8"?> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> """ -url_format = """ <url> +loc_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] +urlset_footer = "</urlset>" + +sitemapindex_header = """<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex + 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"> +""" + +sitemap_format = """ <sitemap> + <loc>{0}</loc> + <lastmod>{1}</lastmod> + </sitemap> +""" + +sitemapindex_footer = "</sitemapindex>" def get_base_path(base): @@ -80,12 +98,12 @@ def get_base_path(base): class Sitemap(LateTask): - """Generate google sitemap.""" + """Generate a sitemap.""" name = "sitemap" def gen_tasks(self): - """Generate Google sitemap.""" + """Generate a sitemap.""" kw = { "base_url": self.site.config["BASE_URL"], "site_url": self.site.config["SITE_URL"], @@ -93,28 +111,32 @@ class Sitemap(LateTask): "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']) + "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm', '.xml', '.rss']), + "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"] } - 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'] + output_path = kw['output_folder'] + sitemapindex_path = os.path.join(output_path, "sitemapindex.xml") + sitemap_path = os.path.join(output_path, "sitemap.xml") + base_path = get_base_path(kw['base_url']) + sitemapindex = {} + urlset = {} + def scan_locs(): - for root, dirs, files in os.walk(output): + for root, dirs, files in os.walk(output, followlinks=True): 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) + lastmod = self.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) + urlset[loc] = loc_format.format(loc, lastmod) for fname in files: if kw['strip_indexes'] and fname == kw['index_file']: continue # We already mapped the folder @@ -124,38 +146,68 @@ class Sitemap(LateTask): if path.endswith(kw['index_file']) and kw['strip_indexes']: # ignore index files when stripping urls continue + if not robot_fetch(path): + 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. + try: + if u'<!doctype html' not in codecs.open(real_path, 'r', 'utf8').read(1024).lower(): + # ignores "html" files without doctype + # alexa-verify, google-site-verification, etc. + continue + except UnicodeDecodeError: + # ignore ancient files + # most non-utf8 files are worthless anyways 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 + """ put RSS in sitemapindex[] instead of in urlset[], sitemap_path is included after it is generated """ + if path.endswith('.xml') or path.endswith('.rss'): + if u'<rss' in codecs.open(real_path, 'r', 'utf8').read(512) or u'<urlset'and path != sitemap_path: + path = path.replace(os.sep, '/') + lastmod = self.get_lastmod(real_path) + loc = urljoin(base_url, base_path + path) + sitemapindex[loc] = sitemap_format.format(loc, lastmod) continue + else: + continue # ignores all XML files except those presumed to be RSS post = self.site.post_per_file.get(path) - if post and (post.is_draft or post.is_retired or post.publish_later): + if post and (post.is_draft or post.is_private or post.publish_later): continue path = path.replace(os.sep, '/') - lastmod = get_lastmod(real_path) + lastmod = self.get_lastmod(real_path) loc = urljoin(base_url, base_path + path) - locs[loc] = url_format.format(loc, lastmod) + urlset[loc] = loc_format.format(loc, lastmod) + + def robot_fetch(path): + for rule in kw["robots_exclusions"]: + robot = robotparser.RobotFileParser() + robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)]) + if not robot.can_fetch("*", '/' + path): + return False # not robot food + return True def write_sitemap(): # Have to rescan, because files may have been added between # task dep scanning and task execution 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>") + outf.write(urlset_header) + for k in sorted(urlset.keys()): + outf.write(urlset[k]) + outf.write(urlset_footer) + sitemap_url = urljoin(base_url, base_path + "sitemap.xml") + sitemapindex[sitemap_url] = sitemap_format.format(sitemap_url, self.get_lastmod(sitemap_path)) + + def write_sitemapindex(): + with codecs.open(sitemapindex_path, 'wb+', 'utf8') as outf: + outf.write(sitemapindex_header) + for k in sorted(sitemapindex.keys()): + outf.write(sitemapindex[k]) + outf.write(sitemapindex_footer) # Yield a task to calculate the dependencies of the sitemap # Other tasks can depend on this output, instead of having # to scan locations. def scan_locs_task(): scan_locs() - return {'locations': list(locs.keys())} + return {'locations': list(urlset.keys()) + list(sitemapindex.keys())} yield { "basename": "_scan_locs", @@ -164,7 +216,7 @@ class Sitemap(LateTask): } yield self.group_task() - task = { + yield { "basename": "sitemap", "name": sitemap_path, "targets": [sitemap_path], @@ -174,7 +226,21 @@ class Sitemap(LateTask): "task_dep": ["render_site"], "calc_dep": ["_scan_locs:sitemap"], } - yield task + yield { + "basename": "sitemap", + "name": sitemapindex_path, + "targets": [sitemapindex_path], + "actions": [(write_sitemapindex,)], + "uptodate": [config_changed(kw)], + "clean": True, + "file_dep": [sitemap_path] + } + + def get_lastmod(self, p): + if self.site.invariant: + return '2014-01-01' + else: + return datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0] if __name__ == '__main__': import doctest diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py index f6b8234..f7f3579 100644 --- a/nikola/plugins/task/tags.py +++ b/nikola/plugins/task/tags.py @@ -61,12 +61,14 @@ class RenderTags(Task): "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'], - "index_display_post_count": - self.site.config['INDEX_DISPLAY_POST_COUNT'], + "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'], "index_teasers": self.site.config['INDEX_TEASERS'], + "generate_rss": self.site.config['GENERATE_RSS'], "rss_teasers": self.site.config["RSS_TEASERS"], - "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'], + "rss_plain": self.site.config["RSS_PLAIN"], + "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], "feed_length": self.site.config['FEED_LENGTH'], + "tzinfo": self.site.tzinfo, } self.site.scan_posts() @@ -81,16 +83,15 @@ class RenderTags(Task): 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 = sorted(posts, key=lambda a: a.date) post_list.reverse() for lang in kw["translations"]: - if kw["hide_untranslated_posts"]: - filtered_posts = [x for x in post_list if x.is_translation_available(lang)] - else: + if kw["show_untranslated_posts"]: filtered_posts = post_list - rss_post_list = [p.source_path for p in filtered_posts] - yield self.tag_rss(tag, lang, rss_post_list, kw, is_category) + else: + filtered_posts = [x for x in post_list if x.is_translation_available(lang)] + if kw["generate_rss"]: + yield self.tag_rss(tag, lang, filtered_posts, kw, is_category) # Render HTML if kw['tag_pages_are_indexes']: yield self.tag_page_as_index(tag, lang, filtered_posts, kw, is_category) @@ -205,12 +206,13 @@ class RenderTags(Task): num_pages = len(lists) for i, post_list in enumerate(lists): context = {} - # On a tag page, the feeds include the tag's feeds - rss_link = ("""<link rel="alternate" type="application/rss+xml" """ - """type="application/rss+xml" title="RSS for tag """ - """{0} ({1})" href="{2}">""".format( - tag, lang, self.site.link(kind + "_rss", tag, lang))) - context['rss_link'] = rss_link + if kw["generate_rss"]: + # On a tag page, the feeds include the tag's feeds + rss_link = ("""<link rel="alternate" type="application/rss+xml" """ + """type="application/rss+xml" title="RSS for tag """ + """{0} ({1})" href="{2}">""".format( + tag, lang, self.site.link(kind + "_rss", tag, lang))) + context['rss_link'] = rss_link output_name = os.path.join(kw['output_folder'], page_name(tag, i, lang)) context["title"] = kw["messages"][lang][ @@ -274,15 +276,13 @@ class RenderTags(Task): def tag_rss(self, tag, lang, posts, kw, is_category): """RSS for a single tag / language""" kind = "category" if is_category else "tag" - #Render RSS + # Render RSS output_name = os.path.normpath( os.path.join(kw['output_folder'], self.site.path(kind + "_rss", tag, lang))) feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/')) deps = [] - post_list = [self.site.global_data[post] for post in posts if - self.site.global_data[post].use_in_feeds] - post_list.sort(key=lambda a: a.date) + post_list = sorted(posts, key=lambda a: a.date) post_list.reverse() for post in post_list: deps += post.deps(lang) @@ -292,9 +292,10 @@ class RenderTags(Task): 'file_dep': deps, 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, - (lang, "{0} ({1})".format(kw["blog_title"], tag), + (lang, "{0} ({1})".format(kw["blog_title"](lang), tag), kw["site_url"], None, post_list, - output_name, kw["rss_teasers"], kw['feed_length'], feed_url))], + output_name, kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], + feed_url))], 'clean': True, 'uptodate': [utils.config_changed(kw)], 'task_dep': ['render_posts'], diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py index f14adfe..097ec96 100644 --- a/nikola/plugins/template/jinja.py +++ b/nikola/plugins/template/jinja.py @@ -51,6 +51,8 @@ class JinjaTemplates(TemplateSystem): if jinja2 is None: return self.lookup = jinja2.Environment() + self.lookup.trim_blocks = True + self.lookup.lstrip_blocks = True self.lookup.filters['tojson'] = json.dumps self.lookup.globals['enumerate'] = enumerate @@ -58,7 +60,19 @@ class JinjaTemplates(TemplateSystem): """Create a template lookup.""" if jinja2 is None: req_missing(['jinja2'], 'use this theme') - self.lookup.loader = jinja2.FileSystemLoader(directories, + self.directories = directories + self.create_lookup() + + def inject_directory(self, directory): + """if it's not there, add the directory to the lookup with lowest priority, and + recreate the lookup.""" + if directory not in self.directories: + self.directories.append(directory) + self.create_lookup() + + def create_lookup(self): + """Create a template lookup object.""" + self.lookup.loader = jinja2.FileSystemLoader(self.directories, encoding='utf-8') def set_site(self, site): diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py index 5a23230..b9d856e 100644 --- a/nikola/plugins/template/mako.py +++ b/nikola/plugins/template/mako.py @@ -50,6 +50,8 @@ class MakoTemplates(TemplateSystem): lookup = None cache = {} filters = {} + directories = [] + cache_dir = None def get_deps(self, filename): text = util.read_file(filename) @@ -65,7 +67,7 @@ class MakoTemplates(TemplateSystem): return deps def set_directories(self, directories, cache_folder): - """Create a template lookup.""" + """Set directories and 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: @@ -74,12 +76,24 @@ class MakoTemplates(TemplateSystem): 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.directories = directories + self.cache_dir = cache_dir + self.create_lookup() + + def inject_directory(self, directory): + """if it's not there, add the directory to the lookup with lowest priority, and + recreate the lookup.""" + if directory not in self.directories: + self.directories.append(directory) + self.create_lookup() + + def create_lookup(self): + """Create a template lookup object.""" self.lookup = TemplateLookup( - directories=directories, - module_directory=cache_dir, + directories=self.directories, + module_directory=self.cache_dir, output_encoding='utf-8') def set_site(self, site): diff --git a/nikola/post.py b/nikola/post.py index 5cf7236..3e3b608 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -37,12 +37,15 @@ try: except ImportError: from urllib.parse import urljoin # NOQA +import dateutil.tz import lxml.html +import natsort try: import pyphen except ImportError: pyphen = None -import pytz + +from math import ceil # for tearDown with _reload we cannot use 'from import' to get forLocaleBorg import nikola.utils @@ -51,18 +54,19 @@ from .utils import ( current_time, Functionary, LOGGER, + LocaleBorg, slugify, to_datetime, unicode_str, demote_headers, get_translation_candidate, + unslugify, ) from .rc4 import rc4 __all__ = ['Post'] TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE) -READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>' class Post(object): @@ -89,17 +93,17 @@ class Post(object): self.compiler = compiler self.compile_html = self.compiler.compile_html self.demote_headers = self.compiler.demote_headers and self.config['DEMOTE_HEADERS'] - tzinfo = pytz.timezone(self.config['TIMEZONE']) + tzinfo = self.config['__tzinfo__'] if self.config['FUTURE_IS_NOW']: self.current_time = None else: - self.current_time = current_time(tzinfo) + self.current_time = current_time() self.translated_to = set([]) self._prev_post = None self._next_post = None self.base_url = self.config['BASE_URL'] self.is_draft = False - self.is_retired = False + self.is_private = False self.is_mathjax = False self.strip_indexes = self.config['STRIP_INDEXES'] self.index_file = self.config['INDEX_FILE'] @@ -115,29 +119,29 @@ class Post(object): self.translations = self.config['TRANSLATIONS'] self.default_lang = self.config['DEFAULT_LANG'] self.messages = messages - self.skip_untranslated = self.config['HIDE_UNTRANSLATED_POSTS'] + self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS'] self._template_name = template_name self.is_two_file = True self.hyphenate = self.config['HYPHENATE'] self._reading_time = None + self._remaining_reading_time = None + self._paragraph_count = None + self._remaining_paragraph_count = None - default_metadata = get_meta(self, self.config['FILE_METADATA_REGEXP']) + default_metadata = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES']) self.meta = Functionary(lambda: None, self.default_lang) self.meta[self.default_lang] = default_metadata # Load internationalized metadata for lang in self.translations: + if os.path.isfile(get_translation_candidate(self.config, self.source_path, lang)): + self.translated_to.add(lang) if lang != self.default_lang: - if os.path.isfile(get_translation_candidate(self.config, self.source_path, lang)): - self.translated_to.add(lang) - meta = defaultdict(lambda: '') meta.update(default_metadata) - meta.update(get_meta(self, self.config['FILE_METADATA_REGEXP'], lang)) + meta.update(get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'], lang)) self.meta[lang] = meta - elif os.path.isfile(self.source_path): - self.translated_to.add(self.default_lang) if not self.is_translation_available(self.default_lang): # Special case! (Issue #373) @@ -147,8 +151,11 @@ class Post(object): if 'date' not in default_metadata and not use_in_feeds: # For stories we don't *really* need a date - default_metadata['date'] = datetime.datetime.utcfromtimestamp( - os.stat(self.source_path).st_ctime).replace(tzinfo=pytz.UTC).astimezone(tzinfo) + if self.config['__invariant__']: + default_metadata['date'] = datetime.datetime(2013, 12, 31, 23, 59, 59, tzinfo=tzinfo) + else: + default_metadata['date'] = datetime.datetime.utcfromtimestamp( + os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(tzinfo) if 'title' not in default_metadata or 'slug' not in default_metadata \ or 'date' not in default_metadata: @@ -169,26 +176,34 @@ class Post(object): self.publish_later = False if self.current_time is None else self.date >= self.current_time is_draft = False - is_retired = False + is_private = False self._tags = {} for lang in self.translated_to: - self._tags[lang] = [x.strip() for x in self.meta[lang]['tags'].split(',')] + self._tags[lang] = natsort.natsorted( + list(set([x.strip() for x in self.meta[lang]['tags'].split(',')]))) self._tags[lang] = [t for t in self._tags[lang] if t] if 'draft' in self._tags[lang]: is_draft = True + LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path)) self._tags[lang].remove('draft') + + # TODO: remove in v8 if 'retired' in self._tags[lang]: - is_retired = True + is_private = True + LOGGER.warning('The "retired" tag in post "{0}" is now deprecated and will be removed in v8. Use "private" instead.'.format(self.source_path)) self._tags[lang].remove('retired') + # end remove in v8 + if 'private' in self._tags[lang]: - is_retired = True + is_private = True + LOGGER.debug('The post "{0}" is private.'.format(self.source_path)) self._tags[lang].remove('private') # While draft comes from the tags, it's not really a tag self.is_draft = is_draft - self.is_retired = is_retired + self.is_private = is_private self.is_post = use_in_feeds - self.use_in_feeds = use_in_feeds and not is_draft and not is_retired \ + self.use_in_feeds = use_in_feeds and not is_draft and not is_private \ and not self.publish_later # If mathjax is a tag, then enable mathjax rendering support @@ -277,6 +292,21 @@ class Post(object): lang = nikola.utils.LocaleBorg().current_lang return self.meta[lang]['title'] + def author(self, lang=None): + """Return localized author or BLOG_AUTHOR if unspecified. + + If lang is not specified, it defaults to the current language from + templates, as set in LocaleBorg. + """ + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + if self.meta[lang]['author']: + author = self.meta[lang]['author'] + else: + author = self.config['BLOG_AUTHOR'](lang) + + return author + def description(self, lang=None): """Return localized description.""" if lang is None: @@ -290,7 +320,6 @@ class Post(object): deps.append(self.base_path) if lang != self.default_lang: deps += [get_translation_candidate(self.config, self.base_path, lang)] - deps += self.fragment_deps(lang) return deps def compile(self, lang): @@ -304,21 +333,31 @@ class Post(object): with codecs.open(path, 'wb+', 'utf8') as outf: outf.write(data) - self.READ_MORE_LINK = self.config['READ_MORE_LINK'] dest = self.translated_base_path(lang) - if not self.is_translation_available(lang) and self.config['HIDE_UNTRANSLATED_POSTS']: + if not self.is_translation_available(lang) and not self.config['SHOW_UNTRANSLATED_POSTS']: return - else: - self.compile_html( - self.translated_source_path(lang), - dest, - self.is_two_file), + # Set the language to the right thing + LocaleBorg().set_locale(lang) + self.compile_html( + self.translated_source_path(lang), + dest, + self.is_two_file), if self.meta('password'): wrap_encrypt(dest, self.meta('password')) if self.publish_later: LOGGER.notice('{0} is scheduled to be published in the future ({1})'.format( self.source_path, self.date)) + def extra_deps(self): + """get extra depepencies from .dep files + This file is created by ReST + """ + dep_path = self.base_path + '.dep' + if os.path.isfile(dep_path): + with codecs.open(dep_path, 'rb+', 'utf8') as depf: + return [l.strip() for l in depf.readlines()] + return [] + def fragment_deps(self, lang): """Return a list of dependencies to build this post's fragment.""" deps = [] @@ -326,10 +365,7 @@ class Post(object): deps.append(self.source_path) if os.path.isfile(self.metadata_path): deps.append(self.metadata_path) - dep_path = self.base_path + '.dep' - if os.path.isfile(dep_path): - with codecs.open(dep_path, 'rb+', 'utf8') as depf: - deps.extend([l.strip() for l in depf.readlines()]) + deps.extend(self.extra_deps()) lang_deps = [] if lang != self.default_lang: lang_deps = [get_translation_candidate(self.config, d, lang) for d in deps] @@ -354,10 +390,7 @@ class Post(object): def translated_base_path(self, lang): """Return path to the translation's base_path file.""" - if lang == self.default_lang: - return self.base_path - else: - return get_translation_candidate(self.config, self.base_path, lang) + return get_translation_candidate(self.config, self.base_path, lang) def _translated_file_path(self, lang): """Return path to the translation's file, or to the original.""" @@ -371,16 +404,30 @@ class Post(object): else: return get_translation_candidate(self.config, self.base_path, sorted(self.translated_to)[0]) - def text(self, lang=None, teaser_only=False, strip_html=False, really_absolute=False): + def text(self, lang=None, teaser_only=False, strip_html=False, show_read_more_link=True, rss_read_more_link=False): """Read the post file for that language and return its contents. teaser_only=True breaks at the teaser marker and returns only the teaser. strip_html=True removes HTML tags + show_read_more_link=False does not add the Read more... link + rss_read_more_link=True uses RSS_READ_MORE_LINK instead of INDEX_READ_MORE_LINK lang=None uses the last used to set locale All links in the returned HTML will be relative. The HTML returned is a bare fragment, not a full document. """ + def strip_root_element(el): + ''' Strips root tag from an Element. + + Required because lxml has an tendency to add <div>, <body> + root tags to strings which are generated by using + lxml.html.tostring() + + :param Element el: the root element to strip + ''' + return (el.text or '') + ''.join( + [lxml.html.tostring(child, encoding='unicode') + for child in el.iterchildren()]) if lang is None: lang = nikola.utils.LocaleBorg().current_lang @@ -395,7 +442,7 @@ class Post(object): return "" # let other errors raise raise(e) - base_url = self.permalink(lang=lang, absolute=really_absolute) + base_url = self.permalink(lang=lang) document.make_links_absolute(base_url) if self.hyphenate: @@ -405,28 +452,34 @@ class Post(object): # data here is a full HTML doc, including HTML and BODY tags # which is not ideal (Issue #464) try: - body = document.body - data = (body.text or '') + ''.join( - [lxml.html.tostring(child, encoding='unicode') - for child in body.iterchildren()]) + data = strip_root_element(document.body) except IndexError: # No body there, it happens sometimes pass if teaser_only: teaser = TEASER_REGEXP.split(data)[0] if teaser != data: - if not strip_html: + if not strip_html and show_read_more_link: if TEASER_REGEXP.search(data).groups()[-1]: teaser += '<p class="more"><a href="{0}">{1}</a></p>'.format( - self.permalink(lang, absolute=really_absolute), + self.permalink(lang), TEASER_REGEXP.search(data).groups()[-1]) else: - teaser += READ_MORE_LINK.format( - link=self.permalink(lang, absolute=really_absolute), - read_more=self.messages[lang]["Read more"]) + l = self.config['RSS_READ_MORE_LINK'](lang) if rss_read_more_link else self.config['INDEX_READ_MORE_LINK'](lang) + teaser += l.format( + link=self.permalink(lang), + read_more=self.messages[lang]["Read more"], + min_remaining_read=self.messages[lang]["%d min remaining to read"] % (self.remaining_reading_time), + reading_time=self.reading_time, + remaining_reading_time=self.remaining_reading_time, + paragraph_count=self.paragraph_count, + remaining_paragraph_count=self.remaining_paragraph_count) # This closes all open tags and sanitizes the broken HTML document = lxml.html.fromstring(teaser) - data = lxml.html.tostring(document, encoding='unicode') + try: + data = strip_root_element(document) + except IndexError: + data = lxml.html.tostring(document, encoding='unicode') if data and strip_html: try: @@ -441,23 +494,71 @@ class Post(object): try: document = lxml.html.fromstring(data) demote_headers(document, self.demote_headers) + data = strip_root_element(document) + except (lxml.etree.ParserError, IndexError): data = lxml.html.tostring(document, encoding='unicode') - except lxml.etree.ParserError: - pass return data @property def reading_time(self): - """Reading time based on length of text. - """ + """Reading time based on length of text.""" if self._reading_time is None: text = self.text(strip_html=True) - words_per_minute = 180 + words_per_minute = 220 words = len(text.split()) - self._reading_time = int(round(words / words_per_minute)) or 1 + self._reading_time = int(ceil(words / words_per_minute)) or 1 return self._reading_time + @property + def remaining_reading_time(self): + """Remaining reading time based on length of text (does not include teaser).""" + if self._remaining_reading_time is None: + text = self.text(teaser_only=True, strip_html=True) + words_per_minute = 220 + words = len(text.split()) + self._remaining_reading_time = self.reading_time - int(ceil(words / words_per_minute)) or 1 + return self._remaining_reading_time + + @property + def paragraph_count(self): + """Return the paragraph count for this post.""" + if self._paragraph_count is None: + # duplicated with Post.text() + lang = nikola.utils.LocaleBorg().current_lang + file_name = self._translated_file_path(lang) + with codecs.open(file_name, "r", "utf8") as post_file: + data = post_file.read().strip() + try: + document = lxml.html.fragment_fromstring(data, "body") + except lxml.etree.ParserError as e: + # if we don't catch this, it breaks later (Issue #374) + if str(e) == "Document is empty": + return "" + # let other errors raise + raise(e) + + # output is a float, for no real reason at all + self._paragraph_count = int(document.xpath('count(//p)')) + return self._paragraph_count + + @property + def remaining_paragraph_count(self): + """Return the remaining paragraph count for this post (does not include teaser).""" + if self._remaining_paragraph_count is None: + try: + # Just asking self.text() is easier here. + document = lxml.html.fragment_fromstring(self.text(teaser_only=True, show_read_more_link=False), "body") + except lxml.etree.ParserError as e: + # if we don't catch this, it breaks later (Issue #374) + if str(e) == "Document is empty": + return "" + # let other errors raise + raise(e) + + self._remaining_paragraph_count = self.paragraph_count - int(document.xpath('count(//p)')) + return self._remaining_paragraph_count + def source_link(self, lang=None): """Return absolute link to the post's source.""" return "/" + self.destination_path( @@ -524,7 +625,7 @@ def re_meta(line, match=None): return (None,) -def _get_metadata_from_filename_by_regex(filename, metadata_regexp): +def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_titles): """ Tries to ried the metadata from the filename based on the given re. This requires to use symbolic group names in the pattern. @@ -538,7 +639,11 @@ def _get_metadata_from_filename_by_regex(filename, metadata_regexp): if match: # .items() for py3k compat. for key, value in match.groupdict().items(): - meta[key.lower()] = value # metadata must be lowercase + k = key.lower().strip() # metadata must be lowercase + if k == 'title' and unslugify_titles: + meta[k] = unslugify(value, discard_numbers=False) + else: + meta[k] = value return meta @@ -620,29 +725,43 @@ def get_metadata_from_meta_file(path, config=None, lang=None): if os.path.isfile(meta_path): with codecs.open(meta_path, "r", "utf8") as meta_file: meta_data = meta_file.readlines() - while len(meta_data) < 7: - meta_data.append("") - (title, slug, date, tags, link, description, _type) = [ - x.strip() for x in meta_data][:7] - - meta = {} - - if title: - meta['title'] = title - if slug: - meta['slug'] = slug - if date: - meta['date'] = date - if tags: - meta['tags'] = tags - if link: - meta['link'] = link - if description: - meta['description'] = description - if _type: - meta['type'] = _type - return meta + # Detect new-style metadata. + newstyleregexp = re.compile(r'\.\. .*?: .*') + newstylemeta = False + for l in meta_data: + if l.strip(): + if re.match(newstyleregexp, l): + newstylemeta = True + + if newstylemeta: + # New-style metadata is basically the same as reading metadata from + # a 1-file post. + return get_metadata_from_file(path, config, lang) + else: + while len(meta_data) < 7: + meta_data.append("") + (title, slug, date, tags, link, description, _type) = [ + x.strip() for x in meta_data][:7] + + meta = {} + + if title: + meta['title'] = title + if slug: + meta['slug'] = slug + if date: + meta['date'] = date + if tags: + meta['tags'] = tags + if link: + meta['link'] = link + if description: + meta['description'] = description + if _type: + meta['type'] = _type + + return meta elif lang: # Metadata file doesn't exist, but not default language, @@ -653,11 +772,12 @@ def get_metadata_from_meta_file(path, config=None, lang=None): return {} -def get_meta(post, file_metadata_regexp=None, lang=None): +def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None): """Get post's meta from source. If ``file_metadata_regexp`` is given it will be tried to read metadata from the filename. + If ``unslugify_titles`` is True, the extracted title (if any) will be unslugified, as is done in galleries. If any metadata is then found inside the file the metadata from the file will override previous findings. """ @@ -676,7 +796,8 @@ def get_meta(post, file_metadata_regexp=None, lang=None): if file_metadata_regexp is not None: meta.update(_get_metadata_from_filename_by_regex(post.source_path, - file_metadata_regexp)) + file_metadata_regexp, + unslugify_titles)) meta.update(get_metadata_from_file(post.source_path, config, lang)) diff --git a/nikola/utils.py b/nikola/utils.py index 46e159e..9420595 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -26,10 +26,11 @@ """Utility functions.""" -from __future__ import print_function, unicode_literals +from __future__ import print_function, unicode_literals, absolute_import from collections import defaultdict, Callable import calendar import datetime +import dateutil.tz import hashlib import locale import logging @@ -39,17 +40,18 @@ import json import shutil import subprocess import sys -from zipfile import ZipFile as zip +from zipfile import ZipFile as zipf try: from imp import reload except ImportError: pass +import dateutil.parser +import dateutil.tz import logbook from logbook.more import ExceptionHandler, ColorizedStderrHandler -import pytz -from . import DEBUG +from nikola import DEBUG class ApplicationWarning(Exception): @@ -70,29 +72,65 @@ def get_logger(name, handlers): l = logbook.Logger(name) for h in handlers: if isinstance(h, list): - l.handlers += h + l.handlers = h else: - l.handlers.append(h) + l.handlers = [h] return l STDERR_HANDLER = [ColorfulStderrHandler( - level=logbook.NOTICE if not DEBUG else logbook.DEBUG, + level=logbook.INFO if not DEBUG else logbook.DEBUG, format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}' )] LOGGER = get_logger('Nikola', STDERR_HANDLER) STRICT_HANDLER = ExceptionHandler(ApplicationWarning, level='WARNING') +# This will block out the default handler and will hide all unwanted +# messages, properly. +logbook.NullHandler().push_application() + if DEBUG: logging.basicConfig(level=logging.DEBUG) else: - logging.basicConfig(level=logging.WARNING) + logging.basicConfig(level=logging.INFO) + + +import warnings + + +def showwarning(message, category, filename, lineno, file=None, line=None): + """Show a warning (from the warnings subsystem) to the user.""" + try: + n = category.__name__ + except AttributeError: + n = str(category) + get_logger(n, STDERR_HANDLER).warn('{0}:{1}: {2}'.format(filename, lineno, message)) + +warnings.showwarning = showwarning def req_missing(names, purpose, python=True, optional=False): - """Log that we are missing some requirements.""" + """Log that we are missing some requirements. + + `names` is a list/tuple/set of missing things. + `purpose` is a string, specifying the use of the missing things. + It completes the sentence: + In order to {purpose}, you must install ... + `python` specifies whether the requirements are Python packages + or other software. + `optional` specifies whether the things are required + (this is an error and we exit with code 5) + or not (this is just a warning). + + Returns the message shown to the user (which you can usually discard). + If no names are specified, False is returned and nothing is shown + to the user. + + """ if not (isinstance(names, tuple) or isinstance(names, list) or isinstance(names, set)): names = (names,) + if not names: + return False if python: whatarethey_s = 'Python package' whatarethey_p = 'Python packages' @@ -121,6 +159,7 @@ if sys.version_info[0] == 3: bytes_str = bytes unicode_str = str unichr = chr + raw_input = input from imp import reload as _reload else: bytes_str = str @@ -130,6 +169,7 @@ else: from doit import tools from unidecode import unidecode +from pkg_resources import resource_filename import PyRSS2Gen as rss @@ -137,9 +177,13 @@ __all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree', 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path', '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary', - 'LocaleBorg', 'sys_encode', 'sys_decode', 'makedirs', - 'get_parent_theme_name', 'ExtendedRSS2', 'demote_headers', - 'get_translation_candidate'] + 'TranslatableSetting', 'TemplateHookRegistry', 'LocaleBorg', + 'sys_encode', 'sys_decode', 'makedirs', 'get_parent_theme_name', + 'demote_headers', 'get_translation_candidate', 'write_metadata', + 'ask', 'ask_yesno'] + +# Are you looking for 'generic_rss_renderer'? +# It's defined in nikola.nikola.Nikola (the site object). ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding @@ -185,10 +229,256 @@ class Functionary(defaultdict): return self[lang][key] +class TranslatableSetting(object): + + """ + A setting that can be translated. + + You can access it via: SETTING(lang). You can omit lang, in which + case Nikola will ask LocaleBorg, unless you set SETTING.lang, + which overrides that call. + + You can also stringify the setting and you will get something + sensible (in what LocaleBorg claims the language is, can also be + overriden by SETTING.lang). Note that this second method is + deprecated. It is kept for backwards compatibility and + safety. It is not guaranteed. + + The underlying structure is a defaultdict. The language that + is the default value of the dict is provided with __init__(). + If you need access the underlying dict (you generally don’t, + """ + + # WARNING: This is generally not used and replaced with a call to + # LocaleBorg(). Set this to a truthy value to override that. + lang = None + + # Note that this setting is global. DO NOT set on a per-instance basis! + default_lang = 'en' + + def __getattribute__(self, attr): + """Return attributes, falling back to string attributes.""" + try: + return super(TranslatableSetting, self).__getattribute__(attr) + except AttributeError: + return self().__getattribute__(attr) + + def __dir__(self): + return list(set(self.__dict__).union(set(dir(str)))) + + def __init__(self, name, inp, translations): + """Initialize a translated setting. + + Valid inputs include: + + * a string -- the same will be used for all languages + * a dict ({lang: value}) -- each language will use the value specified; + if there is none, default_lang is used. + + """ + self.name = name + self._inp = inp + self.translations = translations + self.overriden_default = False + self.values = defaultdict() + + if isinstance(inp, dict): + self.translated = True + self.values.update(inp) + if self.default_lang not in self.values.keys(): + self.default_lang = list(self.values.keys())[0] + self.overridden_default = True + self.values.default_factory = lambda: self.values[self.default_lang] + for k in translations.keys(): + if k not in self.values.keys(): + self.values[k] = inp[self.default_lang] + else: + self.translated = False + self.values[self.default_lang] = inp + self.values.default_factory = lambda: inp + + def get_lang(self): + """Return the language that should be used to retrieve settings.""" + if self.lang: + return self.lang + elif not self.translated: + return self.default_lang + else: + try: + return LocaleBorg().current_lang + except AttributeError: + return self.default_lang + + def __call__(self, lang=None): + """ + Return the value in the requested language. + + While lang is None, self.lang (currently set language) is used. + Otherwise, the standard algorithm is used (see above). + + """ + if lang is None: + return self.values[self.get_lang()] + else: + return self.values[lang] + + def __str__(self): + """Return the value in the currently set language. (deprecated)""" + return self.values[self.get_lang()] + + def __unicode__(self): + """Return the value in the currently set language. (deprecated)""" + return self.values[self.get_lang()] + + def __repr__(self): + """Provide a representation for programmers.""" + return '<TranslatableSetting: {0!r}>'.format(self.name) + + def format(self, *args, **kwargs): + """Format ALL the values in the setting the same way.""" + for l in self.values: + self.values[l] = self.values[l].format(*args, **kwargs) + self.values.default_factory = lambda: self.values[self.default_lang] + return self + + def langformat(self, formats): + """Format ALL the values in the setting, on a per-language basis.""" + if not formats: + # Input is empty. + return self + else: + # This is a little tricky. + # Basically, we have some things that may very well be dicts. Or + # actually, TranslatableSettings in the original unprocessed dict + # form. We need to detect them. + + # First off, we need to check what languages we have and what + # should we use as the default. + keys = list(formats) + if self.default_lang in keys: + d = formats[self.default_lang] + else: + d = formats[keys[0]] + # Discovering languages of the settings here. + langkeys = [] + for f in formats.values(): + for a in f[0] + tuple(f[1].values()): + if isinstance(a, dict): + langkeys += list(a) + # Now that we know all this, we go through all the languages we have. + allvalues = set(keys + langkeys + list(self.values)) + for l in allvalues: + if l in keys: + oargs, okwargs = formats[l] + else: + oargs, okwargs = d + + args = [] + kwargs = {} + + for a in oargs: + # We create temporary TranslatableSettings and replace the + # values with them. + if isinstance(a, dict): + a = TranslatableSetting('NULL', a) + args.append(a(l)) + else: + args.append(a) + + for k, v in okwargs.items(): + if isinstance(v, dict): + v = TranslatableSetting('NULL', v) + kwargs.update({k: v(l)}) + else: + kwargs.update({k: v}) + + self.values[l] = self.values[l].format(*args, **kwargs) + self.values.default_factory = lambda: self.values[self.default_lang] + + return self + + def __getitem__(self, key): + """Provide an alternate interface via __getitem__.""" + return self.values[key] + + def __setitem__(self, key, value): + """Set values for translations.""" + self.values[key] = value + + def __eq__(self, other): + """Test whether two TranslatableSettings are equal.""" + return self.values == other.values + + def __ne__(self, other): + """Test whether two TranslatableSettings are inequal.""" + return self.values != other.values + + +class TemplateHookRegistry(object): + + """ + A registry for template hooks. + + Usage: + + >>> r = TemplateHookRegistry('foo', None) + >>> r.append('Hello!') + >>> r.append(lambda x: 'Hello ' + x + '!', False, 'world') + >>> str(r()) # str() call is not recommended in real use + 'Hello!\\nHello world!' + >>> + """ + + def __init__(self, name, site): + """Initialize a hook registry.""" + self._items = [] + self.name = name + self.site = site + self.context = None + + def generate(self): + """Generate items.""" + for c, inp, site, args, kwargs in self._items: + if c: + if site: + kwargs['site'] = self.site + kwargs['context'] = self.context + yield inp(*args, **kwargs) + else: + yield inp + + def __call__(self): + """Return items, in a string, separated by newlines.""" + return '\n'.join(self.generate()) + + def append(self, inp, wants_site_and_context=False, *args, **kwargs): + """ + Register an item. + + `inp` can be a string or a callable returning one. + `wants_site` tells whether there should be a `site` keyword + argument provided, for accessing the site. + + Further positional and keyword arguments are passed as-is to the + callable. + + `wants_site`, args and kwargs are ignored (but saved!) if `inp` + is not callable. Callability of `inp` is determined only once. + """ + c = callable(inp) + self._items.append((c, inp, wants_site_and_context, args, kwargs)) + + def __hash__(self): + return config_changed({self.name: self._items}) + + def __str__(self): + return '<TemplateHookRegistry: {0}>'.format(self._items) + + class CustomEncoder(json.JSONEncoder): def default(self, obj): try: - return json.JSONEncoder.default(self, obj) + return super(CustomEncoder, self).default(obj) except TypeError: s = repr(obj).split('0x', 1)[0] return s @@ -206,7 +496,9 @@ class config_changed(tools.config_changed): byte_data = data.encode("utf-8") else: byte_data = data - return hashlib.md5(byte_data).hexdigest() + digest = hashlib.md5(byte_data).hexdigest() + # LOGGER.debug('{{"{0}": {1}}}'.format(digest, byte_data)) + return digest else: raise Exception('Invalid type of config_changed parameter -- got ' '{0}, must be string or dict'.format(type( @@ -217,24 +509,23 @@ class config_changed(tools.config_changed): cls=CustomEncoder)) -def get_theme_path(theme): +def get_theme_path(theme, _themes_dir='themes'): """Given a theme name, returns the path where its files are located. Looks in ./themes and in the place where themes go when installed. """ - dir_name = os.path.join('themes', theme) + dir_name = os.path.join(_themes_dir, theme) if os.path.isdir(dir_name): return dir_name - dir_name = os.path.join(os.path.dirname(__file__), - 'data', 'themes', theme) + dir_name = resource_filename('nikola', os.path.join('data', 'themes', theme)) if os.path.isdir(dir_name): return dir_name raise Exception("Can't find theme '{0}'".format(theme)) -def get_template_engine(themes): +def get_template_engine(themes, _themes_dir='themes'): for theme_name in themes: - engine_path = os.path.join(get_theme_path(theme_name), 'engine') + engine_path = os.path.join(get_theme_path(theme_name, _themes_dir), 'engine') if os.path.isfile(engine_path): with open(engine_path) as fd: return fd.readlines()[0].strip() @@ -242,20 +533,20 @@ def get_template_engine(themes): return 'mako' -def get_parent_theme_name(theme_name): - parent_path = os.path.join(get_theme_path(theme_name), 'parent') +def get_parent_theme_name(theme_name, _themes_dir='themes'): + parent_path = os.path.join(get_theme_path(theme_name, _themes_dir), 'parent') if os.path.isfile(parent_path): with open(parent_path) as fd: return fd.readlines()[0].strip() return None -def get_theme_chain(theme): +def get_theme_chain(theme, _themes_dir='themes'): """Create the full theme inheritance chain.""" themes = [theme] while True: - parent = get_parent_theme_name(themes[-1]) + parent = get_parent_theme_name(themes[-1], _themes_dir) # Avoid silly loops if parent is None or parent in themes: break @@ -266,6 +557,15 @@ def get_theme_chain(theme): warned = [] +class LanguageNotFoundError(Exception): + def __init__(self, lang, orig): + self.lang = lang + self.orig = orig + + def __str__(self): + return 'cannot find language {0}'.format(self.lang) + + def load_messages(themes, translations, default_lang): """ Load theme's messages into context. @@ -281,18 +581,23 @@ def load_messages(themes, translations, default_lang): sys.path.insert(0, msg_folder) english = __import__('messages_en') for lang in list(translations.keys()): - # If we don't do the reload, the module is cached - translation = __import__('messages_' + lang) - reload(translation) - if sorted(translation.MESSAGES.keys()) !=\ - sorted(english.MESSAGES.keys()) and \ - lang not in warned: - warned.append(lang) - LOGGER.warn("Incomplete translation for language " - "'{0}'.".format(lang)) - messages[lang].update(english.MESSAGES) - messages[lang].update(translation.MESSAGES) - del(translation) + try: + translation = __import__('messages_' + lang) + # If we don't do the reload, the module is cached + reload(translation) + if sorted(translation.MESSAGES.keys()) !=\ + sorted(english.MESSAGES.keys()) and \ + lang not in warned: + warned.append(lang) + LOGGER.warn("Incomplete translation for language " + "'{0}'.".format(lang)) + messages[lang].update(english.MESSAGES) + for k, v in translation.MESSAGES.items(): + if v: + messages[lang][k] = v + del(translation) + except ImportError as orig: + raise LanguageNotFoundError(lang, orig) sys.path = oldpath return messages @@ -314,7 +619,7 @@ def copy_tree(src, dst, link_cutoff=None): """ ignore = set(['.svn']) base_len = len(src.split(os.sep)) - for root, dirs, files in os.walk(src): + for root, dirs, files in os.walk(src, followlinks=True): root_parts = root.split(os.sep) if set(root_parts) & ignore: continue @@ -325,12 +630,8 @@ def copy_tree(src, dst, link_cutoff=None): continue dst_file = os.path.join(dst_dir, src_name) src_file = os.path.join(root, src_name) - if sys.version_info[0] == 2: - # Python2 prefers encoded str here - dst_file = sys_encode(dst_file) - src_file = sys_encode(src_file) yield { - 'name': str(dst_file), + 'name': dst_file, 'file_dep': [src_file], 'targets': [dst_file], 'actions': [(copy_file, (src_file, dst_file, link_cutoff))], @@ -397,11 +698,14 @@ def slugify(value): return _slugify_hyphenate_re.sub('-', value) -def unslugify(value): - """ - Given a slug string (as a filename), return a human readable string +def unslugify(value, discard_numbers=True): + """Given a slug string (as a filename), return a human readable string. + + If discard_numbers is True, numbers right at the beginning of input + will be removed. """ - value = re.sub('^[0-9]+', '', value) + if discard_numbers: + value = re.sub('^[0-9]+', '', value) value = re.sub('([_\-\.])', ' ', value) value = value.strip().capitalize() return value @@ -418,55 +722,31 @@ def extract_all(zipfile, path='themes'): pwd = os.getcwd() makedirs(path) os.chdir(path) - with zip(zipfile) as z: - namelist = z.namelist() - for f in namelist: - if f.endswith('/') and '..' in f: - raise UnsafeZipException('The zip file contains ".." and is ' - 'not safe to expand.') - for f in namelist: - if f.endswith('/'): - makedirs(f) - else: - z.extract(f) + z = zipf(zipfile) + namelist = z.namelist() + for f in namelist: + if f.endswith('/') and '..' in f: + raise UnsafeZipException('The zip file contains ".." and is ' + 'not safe to expand.') + for f in namelist: + if f.endswith('/'): + makedirs(f) + else: + z.extract(f) + z.close() os.chdir(pwd) -# From https://github.com/lepture/liquidluck/blob/develop/liquidluck/utils.py def to_datetime(value, tzinfo=None): - if isinstance(value, datetime.datetime): - return value - supported_formats = [ - '%Y/%m/%d %H:%M', - '%Y/%m/%d %H:%M:%S', - '%Y/%m/%d %I:%M:%S %p', - '%a %b %d %H:%M:%S %Y', - '%Y-%m-%d %H:%M:%S', - '%Y-%m-%d %H:%M', - '%Y-%m-%dT%H:%M', - '%Y%m%d %H:%M:%S', - '%Y%m%d %H:%M', - '%Y-%m-%d', - '%Y%m%d', - ] - for format in supported_formats: - try: - dt = datetime.datetime.strptime(value, format) - if tzinfo is None: - return dt - # Build a localized time by using a given time zone. - return tzinfo.localize(dt) - except ValueError: - pass - # So, let's try dateutil try: - from dateutil import parser - dt = parser.parse(value) - if tzinfo is None or dt.tzinfo: - return dt - return tzinfo.localize(dt) - except ImportError: - raise ValueError('Unrecognized date/time: {0!r}, try installing dateutil...'.format(value)) + if not isinstance(value, datetime.datetime): + # dateutil does bad things with TZs like UTC-03:00. + dateregexp = re.compile(r' UTC([+-][0-9][0-9]:[0-9][0-9])') + value = re.sub(dateregexp, r'\1', value) + value = dateutil.parser.parse(value) + if not value.tzinfo: + value = value.replace(tzinfo=tzinfo) + return value except Exception: raise ValueError('Unrecognized date/time: {0!r}'.format(value)) @@ -474,28 +754,18 @@ def to_datetime(value, tzinfo=None): def get_tzname(dt): """ Given a datetime value, find the name of the time zone. - """ - try: - from dateutil import tz - except ImportError: - raise ValueError('Unrecognized date/time: {0!r}, try installing dateutil...'.format(dt)) - tzoffset = dt.strftime('%z') - for name in pytz.common_timezones: - timezone = tz.gettz(name) - now = dt.now(timezone) - offset = now.strftime('%z') - if offset == tzoffset: - return name - raise ValueError('Unrecognized date/time: {0!r}'.format(dt)) + DEPRECATED: This thing returned basically the 1st random zone + that matched the offset. + """ + return dt.tzname() def current_time(tzinfo=None): - dt = datetime.datetime.utcnow() if tzinfo is not None: - dt = tzinfo.fromutc(dt) + dt = datetime.datetime.now(tzinfo) else: - dt = pytz.UTC.localize(dt) + dt = datetime.datetime.now(dateutil.tz.tzlocal()) return dt @@ -589,7 +859,7 @@ def get_crumbs(path, is_file=False, index_folder=None): return list(reversed(_crumbs)) -def get_asset_path(path, themes, files_folders={'files': ''}): +def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='themes'): """ .. versionchanged:: 6.1.0 @@ -599,22 +869,22 @@ def get_asset_path(path, themes, files_folders={'files': ''}): If the asset is not provided by a theme, then it will be checked for in the FILES_FOLDERS - >>> print(get_asset_path('assets/css/rst.css', ['bootstrap', 'base'])) # doctest: +SKIP - [...]/nikola/data/themes/base/assets/css/rst.css + >>> print(get_asset_path('assets/css/rst.css', ['bootstrap', 'base'])) + /.../nikola/data/themes/base/assets/css/rst.css - >>> print(get_asset_path('assets/css/theme.css', ['bootstrap', 'base'])) # doctest: +SKIP - [...]/nikola/data/themes/bootstrap/assets/css/theme.css + >>> print(get_asset_path('assets/css/theme.css', ['bootstrap', 'base'])) + /.../nikola/data/themes/bootstrap/assets/css/theme.css - >>> print(get_asset_path('nikola.py', ['bootstrap', 'base'], {'nikola': ''})) # doctest: +SKIP - [...]/nikola/nikola.py + >>> print(get_asset_path('nikola.py', ['bootstrap', 'base'], {'nikola': ''})) + /.../nikola/nikola.py - >>> print(get_asset_path('nikola/nikola.py', ['bootstrap', 'base'], {'nikola':'nikola'})) # doctest: +SKIP - [...]/nikola/nikola.py + >>> print(get_asset_path('nikola/nikola.py', ['bootstrap', 'base'], {'nikola':'nikola'})) + None """ for theme_name in themes: candidate = os.path.join( - get_theme_path(theme_name), + get_theme_path(theme_name, _themes_dir), path ) if os.path.isfile(candidate): @@ -628,6 +898,11 @@ def get_asset_path(path, themes, files_folders={'files': ''}): return None +class LocaleBorgUninitializedException(Exception): + def __init__(self): + super(LocaleBorgUninitializedException, self).__init__("Attempt to use LocaleBorg before initialization") + + class LocaleBorg(object): """ Provides locale related services and autoritative current_lang, @@ -662,6 +937,9 @@ class LocaleBorg(object): Examples: "Spanish", "French" can't do the full circle set / get / set That used to break calendar, but now seems is not the case, with month at least """ + + initialized = False + @classmethod def initialize(cls, locales, initial_lang): """ @@ -696,7 +974,7 @@ class LocaleBorg(object): def __init__(self): if not self.initialized: - raise Exception("Attempt to use LocaleBorg before initialization") + raise LocaleBorgUninitializedException() self.__dict__ = self.__shared_state def set_locale(self, lang): @@ -734,26 +1012,10 @@ class LocaleBorg(object): return s -class ExtendedRSS2(rss.RSS2): - def publish_extensions(self, handler): - if self.self_url: - handler.startElement("atom:link", { - 'href': self.self_url, - 'rel': "self", - 'type': "application/rss+xml" - }) - handler.endElement("atom:link") - - class ExtendedItem(rss.RSSItem): def __init__(self, **kw): - author = kw.pop('author') - if author and '@' in author[1:]: # Yes, this is a silly way to validate an email - kw['author'] = author - self.creator = None - else: - self.creator = author + self.creator = kw.pop('creator') # It's an old style class return rss.RSSItem.__init__(self, **kw) @@ -824,9 +1086,196 @@ def get_root_dir(): def get_translation_candidate(config, path, lang): """ Return a possible path where we can find the translated version of some page - based on the TRANSLATIONS_PATTERN configuration variable + based on the TRANSLATIONS_PATTERN configuration variable. + + >>> config = {'TRANSLATIONS_PATTERN': '{path}.{lang}.{ext}', 'DEFAULT_LANG': 'en', 'TRANSLATIONS': {'es':'1', 'en': 1}} + >>> print(get_translation_candidate(config, '*.rst', 'es')) + *.es.rst + >>> print(get_translation_candidate(config, 'fancy.post.rst', 'es')) + fancy.post.es.rst + >>> print(get_translation_candidate(config, '*.es.rst', 'es')) + *.es.rst + >>> print(get_translation_candidate(config, '*.es.rst', 'en')) + *.rst + >>> print(get_translation_candidate(config, 'cache/posts/fancy.post.es.html', 'en')) + cache/posts/fancy.post.html + >>> print(get_translation_candidate(config, 'cache/posts/fancy.post.html', 'es')) + cache/posts/fancy.post.es.html + >>> print(get_translation_candidate(config, 'cache/stories/charts.html', 'es')) + cache/stories/charts.es.html + >>> print(get_translation_candidate(config, 'cache/stories/charts.html', 'en')) + cache/stories/charts.html + + >>> config = {'TRANSLATIONS_PATTERN': '{path}.{ext}.{lang}', 'DEFAULT_LANG': 'en', 'TRANSLATIONS': {'es':'1', 'en': 1}} + >>> print(get_translation_candidate(config, '*.rst', 'es')) + *.rst.es + >>> print(get_translation_candidate(config, '*.rst.es', 'es')) + *.rst.es + >>> print(get_translation_candidate(config, '*.rst.es', 'en')) + *.rst + >>> print(get_translation_candidate(config, 'cache/posts/fancy.post.html.es', 'en')) + cache/posts/fancy.post.html + >>> print(get_translation_candidate(config, 'cache/posts/fancy.post.html', 'es')) + cache/posts/fancy.post.html.es + """ + # FIXME: this is rather slow and this function is called A LOT + # Convert the pattern into a regexp pattern = config['TRANSLATIONS_PATTERN'] - path, ext = os.path.splitext(path) - ext = ext[1:] if len(ext) > 0 else ext - return pattern.format(path=path, lang=lang, ext=ext) + # This will still break if the user has ?*[]\ in the pattern. But WHY WOULD HE? + pattern = pattern.replace('.', r'\.') + pattern = pattern.replace('{path}', '(?P<path>.+?)') + pattern = pattern.replace('{ext}', '(?P<ext>[^\./]+)') + pattern = pattern.replace('{lang}', '(?P<lang>{0})'.format('|'.join(config['TRANSLATIONS'].keys()))) + m = re.match(pattern, path) + if m and all(m.groups()): # It's a translated path + p, e, l = m.group('path'), m.group('ext'), m.group('lang') + if l == lang: # Nothing to do + return path + elif lang == config['DEFAULT_LANG']: # Return untranslated path + return '{0}.{1}'.format(p, e) + else: # Change lang and return + return config['TRANSLATIONS_PATTERN'].format(path=p, ext=e, lang=lang) + else: + # It's a untranslated path, assume it's path.ext + p, e = os.path.splitext(path) + e = e[1:] # No initial dot + if lang == config['DEFAULT_LANG']: # Nothing to do + return path + else: # Change lang and return + return config['TRANSLATIONS_PATTERN'].format(path=p, ext=e, lang=lang) + + +def write_metadata(data): + """Write metadata.""" + order = ('title', 'slug', 'date', 'tags', 'link', 'description', 'type') + f = '.. {0}: {1}' + meta = [] + for k in order: + try: + meta.append(f.format(k, data.pop(k))) + except KeyError: + pass + + # Leftover metadata (user-specified/non-default). + for k, v in data.items(): + meta.append(f.format(k, v)) + + meta.append('') + + return '\n'.join(meta) + + +def ask(query, default=None): + """Ask a question.""" + if default: + default_q = ' [{0}]'.format(default) + else: + default_q = '' + inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip() + if inp or default is None: + return inp + else: + return default + + +def ask_yesno(query, default=None): + """Ask a yes/no question.""" + if default is None: + default_q = ' [y/n]' + elif default is True: + default_q = ' [Y/n]' + elif default is False: + default_q = ' [y/N]' + inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q)).strip() + if inp: + return inp.lower().startswith('y') + elif default is not None: + return default + else: + # Loop if no answer and no default. + return ask_yesno(query, default) + + +from nikola.plugin_categories import Command +from doit.cmdparse import CmdParse + + +class CommandWrapper(object): + """Converts commands into functions.""" + + def __init__(self, cmd, commands_object): + self.cmd = cmd + self.commands_object = commands_object + + def __call__(self, *args, **kwargs): + if args or (not args and not kwargs): + self.commands_object._run([self.cmd] + list(args)) + else: + # Here's where the keyword magic would have to go + self.commands_object._run_with_kw(self.cmd, *args, **kwargs) + + +class Commands(object): + + """Nikola Commands. + + Sample usage: + >>> commands.check('-l') # doctest: +SKIP + + Or, if you know the internal argument names: + >>> commands.check(list=True) # doctest: +SKIP + """ + + def __init__(self, main): + """Takes a main instance, works as wrapper for commands.""" + self._cmdnames = [] + for k, v in main.get_commands().items(): + self._cmdnames.append(k) + if k in ['run', 'init']: + continue + if sys.version_info[0] == 2: + k2 = bytes(k) + else: + k2 = k + nc = type( + k2, + (CommandWrapper,), + { + '__doc__': options2docstring(k, main.sub_cmds[k].options) + }) + setattr(self, k, nc(k, self)) + self.main = main + + def _run(self, cmd_args): + self.main.run(cmd_args) + + def _run_with_kw(self, cmd, *a, **kw): + cmd = self.main.sub_cmds[cmd] + options, _ = CmdParse(cmd.options).parse([]) + options.update(kw) + if isinstance(cmd, Command): + cmd.execute(options=options, args=a) + else: # Doit command + cmd.execute(options, a) + + def __repr__(self): + """Return useful and verbose help.""" + + return """\ +<Nikola Commands> + + Sample usage: + >>> commands.check('-l') + + Or, if you know the internal argument names: + >>> commands.check(list=True) + +Available commands: {0}.""".format(', '.join(self._cmdnames)) + + +def options2docstring(name, options): + result = ['Function wrapper for command %s' % name, 'arguments:'] + for opt in options: + result.append('{0} type {1} default {2}'.format(opt.name, opt.type.__name__, opt.default)) + return '\n'.join(result) diff --git a/nikola/winutils.py b/nikola/winutils.py index 517a326..712de39 100644 --- a/nikola/winutils.py +++ b/nikola/winutils.py @@ -26,74 +26,92 @@ """windows utilities to workaround problems with symlinks in a git clone""" +from __future__ import print_function, unicode_literals import os import shutil -import sys -# don't add imports outside stdlib, will be imported in setup.py +# don't add imports to nikola code, will be imported in setup.py -def should_fix_git_symlinked(): - """True if git symlinls markers should be filled with the real content""" - if sys.platform == 'win32': - path = (os.path.dirname(__file__) + - r'\data\samplesite\stories\theming.rst') - try: - if os.path.getsize(path) < 200: - return True - except Exception: - pass - return False +def is_file_into_dir(filename, dirname): + try: + res = not os.path.relpath(filename, dirname).startswith('.') + except ValueError: + res = False + return res -def fix_git_symlinked(src, dst): - """fix git symlinked files in windows that had been copied from src to dst +def fix_all_git_symlinked(topdir): + """inplace conversion of git symlinks to real content Most (all?) of git implementations in windows store a symlink pointing into the repo as a text file, the text being the relative path to the file with the real content. So, in a clone of nikola in windows the symlinked files will have the - wrong content. + wrong content; a .zip download from Github has the same problem. - The linux usage pattern for those files is 'copy to some dir, then use', - so we inspect after the copy and rewrite the wrong contents. + This function will rewrite each symlinked file with the correct contents, but + keep in mind that the working copy will be seen as dirty by git after operation. - The goals are: - support running nikola from a clone without installing and without - making dirty the WC. + Expects to find a list of symlinked files at nikola/data/symlinked.txt - support install from the WC. + The list can be generated by scripts/generate_symlinked_list.sh , which is + basically a redirect of + cd nikola_checkout + git ls-files -s | awk '/120000/{print $4}' - if possible and needed, support running the test suite without making - dirty the WC. + Weakness: if interrupted of fail amidst a directory copy, next run will not + see the missing files. """ - # if running from WC there should be a 'doc' dir sibling to nikola package - if not should_fix_git_symlinked(): - return - # probabbly in a WC, so symlinks should be fixed - for root, dirs, files in os.walk(dst): - for name in files: - filename = os.path.join(root, name) - - # detect if symlinked - try: - if not (2 < os.path.getsize(filename) < 500): - continue - # which encoding uses a git symlink marker ? betting on default - with open(filename, 'r') as f: - text = f.read() - if text[0] != '.': - # de facto hint to skip binary files and exclude.meta - continue - except Exception: - # probably encoding: content binary or encoding not defalt, - # also in py2.6 it can be path encoding + with open(topdir + r'\nikola\data\symlinked.txt', 'rb') as f: + all_bytes = f.read() + text = all_bytes.decode('utf8') + # expect each line a relpath from git or zip root, + # smoke test relpaths are relative to git root + if text.startswith('.'): + raise Exception(r'Bad data in \nikola\data\symlinked.txt') + relnames = text.split('\n') + relnames = [name.strip().replace('/', '\\') for name in relnames] + relnames = [name for name in relnames if name] + + failures = 0 + for name in relnames: + # build dst path and do some basic validation + dst = os.path.join(topdir, name) + # don't access files outside topdir + if not is_file_into_dir(dst, topdir): + continue + if os.path.isdir(dst): + # assume the file was de-symlinked + continue + + # build src path and do some basic validation + with open(os.path.join(topdir, dst), 'r') as f: + text = f.read() + dst_dir = os.path.dirname(dst) + try: + src = os.path.normpath(os.path.join(dst_dir, text)) + if not os.path.exists(src): + # assume the file was de-symlinked before continue - dst_dir_relpath = os.path.dirname(os.path.relpath(filename, dst)) - path = os.path.normpath(os.path.join(src, dst_dir_relpath, text)) - if not os.path.exists(path): + # don't access files outside topdir + if not is_file_into_dir(src, topdir): continue - # most probably it is a git symlinked file + except Exception: + # assume the file was de-symlinked before + continue + + # copy src to dst + try: + if os.path.isdir(src): + os.unlink(dst) + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + except Exception: + failures += 1 + print("*** copy failed for") + print("\t src:", src) + print("\t dst:", dst) - # copy original content to filename - shutil.copy(path, filename) + return failures |
