diff options
Diffstat (limited to 'nikola')
303 files changed, 3302 insertions, 3088 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py index 263874f..4ab6e34 100644 --- a/nikola/__init__.py +++ b/nikola/__init__.py @@ -24,10 +24,12 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Nikola -- a modular, fast, simple, static website generator.""" + from __future__ import absolute_import import os -__version__ = "7.6.0" +__version__ = "7.6.4" DEBUG = bool(os.getenv('NIKOLA_DEBUG')) from .nikola import Nikola # NOQA diff --git a/nikola/__main__.py b/nikola/__main__.py index 6aa0977..2aa63f4 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The main function of Nikola.""" + from __future__ import print_function, unicode_literals from collections import defaultdict import os @@ -49,7 +51,7 @@ from blinker import signal from . import __version__ from .plugin_categories import Command from .nikola import Nikola -from .utils import sys_decode, sys_encode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, ColorfulStderrHandler +from .utils import sys_decode, sys_encode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, STDERR_HANDLER, ColorfulStderrHandler if sys.version_info[0] == 3: import importlib.machinery @@ -63,6 +65,7 @@ _RETURN_DOITNIKOLA = False def main(args=None): + """Run Nikola.""" colorful = False if sys.stderr.isatty() and os.name != 'nt': colorful = True @@ -88,13 +91,18 @@ def main(args=None): break quiet = False + strict = False if len(args) > 0 and args[0] == 'build' and '--strict' in args: LOGGER.notice('Running in strict mode') STRICT_HANDLER.push_application() + strict = True if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args: - nullhandler = NullHandler() - nullhandler.push_application() + NullHandler().push_application() quiet = True + if not quiet and not strict: + NullHandler().push_application() + STDERR_HANDLER[0].push_application() + global config original_cwd = os.getcwd() @@ -167,11 +175,12 @@ def main(args=None): class Help(DoitHelp): - """show Nikola usage.""" + + """Show Nikola usage.""" @staticmethod def print_usage(cmds): - """print nikola "usage" (basic help) instructions""" + """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 @@ -190,8 +199,11 @@ class Help(DoitHelp): class Build(DoitRun): - """expose "run" command as "build" for backward compatibility""" + + """Expose "run" command as "build" for backwards compatibility.""" + def __init__(self, *args, **kw): + """Initialize Build.""" opts = list(self.cmd_options) opts.append( { @@ -226,9 +238,11 @@ class Build(DoitRun): class Clean(DoitClean): - """A clean that removes cache/""" + + """Clean site, including the cache directory.""" def clean_tasks(self, tasks, dryrun): + """Clean tasks.""" if not dryrun and config: cache_folder = config.get('CACHE_FOLDER', 'cache') if os.path.exists(cache_folder): @@ -241,12 +255,16 @@ DoitAuto.name = 'doit_auto' class NikolaTaskLoader(TaskLoader): - """custom task loader to get tasks from Nikola instead of dodo.py file""" + + """Nikola-specific task loader.""" + def __init__(self, nikola, quiet=False): + """Initialize the loader.""" self.nikola = nikola self.quiet = quiet def load_tasks(self, cmd, opt_values, pos_args): + """Load Nikola tasks.""" if self.quiet: DOIT_CONFIG = { 'verbosity': 0, @@ -270,17 +288,22 @@ class NikolaTaskLoader(TaskLoader): class DoitNikola(DoitMain): + + """Nikola-specific implementation of DoitMain.""" + # overwite help command DOIT_CMDS = list(DoitMain.DOIT_CMDS) + [Help, Build, Clean, DoitAuto] TASK_LOADER = NikolaTaskLoader def __init__(self, nikola, quiet=False): + """Initialzie DoitNikola.""" super(DoitNikola, self).__init__() self.nikola = nikola nikola.doit = self self.task_loader = self.TASK_LOADER(nikola, quiet) def get_cmds(self): + """Get commands.""" # core doit commands cmds = DoitMain.get_cmds(self) # load nikola commands @@ -289,7 +312,7 @@ class DoitNikola(DoitMain): return cmds def run(self, cmd_args): - sub_cmds = self.get_cmds() + """Run Nikola.""" args = self.process_args(cmd_args) args = [sys_decode(arg) for arg in args] @@ -311,17 +334,31 @@ class DoitNikola(DoitMain): if arg not in ('--help', '-h'): args.append(arg) + if args[0] == 'help': + self.nikola.init_plugins(commands_only=True) + else: + self.nikola.init_plugins() + + sub_cmds = self.get_cmds() + if any(arg in ("--version", '-V') for arg in args): cmd_args = ['version'] args = ['version'] if args[0] not in sub_cmds.keys(): LOGGER.error("Unknown command {0}".format(args[0])) sugg = defaultdict(list) - for c in sub_cmds.keys(): - d = lev(c, args[0]) + sub_filtered = (i for i in sub_cmds.keys() if i != 'run') + for c in sub_filtered: + d = levenshtein(c, args[0]) sugg[d].append(c) - LOGGER.info('Did you mean "{}"?', '" or "'.join(sugg[min(sugg.keys())])) + if sugg.keys(): + best_sugg = sugg[min(sugg.keys())] + if len(best_sugg) == 1: + LOGGER.info('Did you mean "{}"?'.format(best_sugg[0])) + else: + LOGGER.info('Did you mean "{}" or "{}"?'.format('", "'.join(best_sugg[:-1]), best_sugg[-1])) return 3 + if sub_cmds[args[0]] is not Help and not isinstance(sub_cmds[args[0]], Command): # Is a doit command if not self.nikola.configured: LOGGER.error("This command needs to run inside an " @@ -331,15 +368,36 @@ class DoitNikola(DoitMain): @staticmethod def print_version(): + """Print Nikola version.""" print("Nikola v" + __version__) -# Stolen from http://stackoverflow.com/questions/4173579/implementing-levenshtein-distance-in-python -def lev(a, b): - if not a or not b: - return max(len(a), len(b)) - return min(lev(a[1:], b[1:]) + (a[0] != b[0]), lev(a[1:], b) + 1, lev(a, b[1:]) + 1) - +def levenshtein(s1, s2): + u"""Calculate the Levenshtein distance of two strings. + + Implementation from Wikibooks: + https://en.wikibooks.org/w/index.php?title=Algorithm_Implementation/Strings/Levenshtein_distance&oldid=2974448#Python + Copyright © The Wikibooks contributors (CC BY-SA/fair use citation); edited to match coding style and add an exception. + """ + if len(s1) < len(s2): + return levenshtein(s2, s1) + + # len(s1) >= len(s2) + if len(s2) == 0: + return len(s1) + + previous_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + # j+1 instead of j since previous_row and current_row are one character longer than s2 + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/nikola/conf.py.in b/nikola/conf.py.in index ab00673..b7a88f7 100644 --- a/nikola/conf.py.in +++ b/nikola/conf.py.in @@ -21,7 +21,7 @@ import time 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 +# in a prominent link. Don't forget the protocol (http/https)! SITE_URL = ${SITE_URL} # This is the URL where Nikola's output will be deployed. # If not set, defaults to SITE_URL @@ -229,7 +229,7 @@ WRITE_TAG_CLOUD = True # "blogging": "Meta-blog posts about blogging about blogging.", # "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." # }, -#} +# } # If you do not want to display a tag publicly, you can mark it as hidden. @@ -255,10 +255,10 @@ HIDDEN_TAGS = ['mathjax'] # using a forward slash ('/') to separate paths. Use a backslash ('\') to escape # a forward slash or a backslash (i.e. '\//\\' is a path specifying the # subcategory called '\' of the top-level category called '/'). -# CATEGORY_ALLOW_HIERARCHIES = False +CATEGORY_ALLOW_HIERARCHIES = ${CATEGORY_ALLOW_HIERARCHIES} # If CATEGORY_OUTPUT_FLAT_HIERARCHY is set to True, the output written to output # contains only the name of the leaf category and not the whole path. -# CATEGORY_OUTPUT_FLAT_HIERARCHY = False +CATEGORY_OUTPUT_FLAT_HIERARCHY = ${CATEGORY_OUTPUT_FLAT_HIERARCHY} # If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain # the posts themselves. If set to False, it will be just a list of links. @@ -272,7 +272,7 @@ HIDDEN_TAGS = ['mathjax'] # "blogging": "Meta-blog posts about blogging about blogging.", # "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." # }, -#} +# } # If you do not want to display a category publicly, you can mark it as hidden. # The category will not be displayed on the category list page. @@ -546,7 +546,7 @@ IMAGE_FOLDERS = {'images': 'images'} # ("icon", "/icon_128x128.png", "128x128"), # ) -# Show only teasers in the index pages? Defaults to False. +# Show teasers (instead of full posts) in indexes? Defaults to False. # INDEX_TEASERS = False # HTML fragments with the Read more... links. @@ -582,10 +582,10 @@ LICENSE = "" # I recommend using the Creative Commons' wizard: # http://creativecommons.org/choose/ # LICENSE = """ -# <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> +# <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"> # <img alt="Creative Commons License BY-NC-SA" # style="border-width:0; margin-bottom:12px;" -# src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" +# src="http://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"></a>""" # A small copyright notice for the page footer (in HTML). # (translatable) @@ -655,8 +655,7 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} # (Uses the INDEX_FILE setting, so if that is, say, default.html, # it will instead /foo/default.html => /foo) # (Note: This was briefly STRIP_INDEX_HTML in v 5.4.3 and 5.4.4) -# Default = False -# STRIP_INDEXES = False +STRIP_INDEXES = ${STRIP_INDEXES} # Should the sitemap list directories which only include other directories # and no files. @@ -782,7 +781,7 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] # change it for a FeedBurner feed or something else. # RSS_LINK = None -# Show only teasers in the RSS and Atom feeds? Default to True +# Show teasers (instead of full posts) in feeds? Defaults to True. # RSS_TEASERS = True # Strip HTML in the RSS feed? Default to False @@ -799,29 +798,34 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] # it appears on the navigation bar: # # SEARCH_FORM = """ -# <!-- Custom search --> +# <!-- DuckDuckGo 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="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;" /> +# 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="//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"/> +# <!-- Google custom search --> +# <form method="get" action="http://www.google.com/search" class="navbar-form navbar-right" role="search"> +# <div class="form-group"> +# <input type="text" name="q" class="form-control" placeholder="Search"> +# </div> +# <button type="submit" class="btn btn-primary"> +# <span class="glyphicon glyphicon-search"></span> +# </button> +# <input type="hidden" name="sitesearch" value="%s"> # </form> # <!-- End of custom search --> -#""" % SITE_URL +# """ % SITE_URL # Use content distribution networks for jQuery, twitter-bootstrap css and js, # and html5shiv (for older versions of Internet Explorer) @@ -926,26 +930,6 @@ UNSLUGIFY_TITLES = True # sometimes crash Nikola, your web server, or eat your cat. # USE_SLUGIFY = True -# You can configure the logging handlers installed as plugins or change the -# 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': '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. # Consult your engine's documentation on filters if you need help defining # those. diff --git a/nikola/data/samplesite/README.txt b/nikola/data/samplesite/README.txt index da0d685..011a882 100644 --- a/nikola/data/samplesite/README.txt +++ b/nikola/data/samplesite/README.txt @@ -2,7 +2,7 @@ This folder contains the source used to generate a static site using Nikola. Installation and documentation at https://getnikola.com/ -Configuration file for the site is `conf.py`. +Configuration file for the site is ``conf.py``. To build the site:: @@ -10,10 +10,7 @@ To build the site:: To see it:: - nikola serve - -And point your browser to http://localhost:8000/ - + nikola serve -b To check all available commands:: diff --git a/nikola/data/samplesite/posts/1.rst b/nikola/data/samplesite/posts/1.rst index 9a55859..88263a7 100644 --- a/nikola/data/samplesite/posts/1.rst +++ b/nikola/data/samplesite/posts/1.rst @@ -17,11 +17,11 @@ and build a site using it. Congratulations! Next steps: -* `Read the manual </stories/handbook.html>`__ +* :doc:`Read the manual <handbook>` * `Visit the Nikola website to learn more <https://getnikola.com>`__ -* `See a demo photo gallery </galleries/demo/index.html>`__ -* `See a demo listing </stories/listings-demo.html>`__ -* `See a demo slideshow </stories/slides-demo.html>`__ -* `See a demo of the Bootstrap theme </stories/bootstrap-demo.html>`__ +* `See a demo photo gallery <link://gallery/demo>`__ +* :doc:`See a demo listing <listings-demo>` +* :doc:`See a demo slideshow <slides-demo>` +* :doc:`See a demo of the Bootstrap theme <bootstrap-demo>` Send feedback to info@getnikola.com! diff --git a/nikola/data/symlinked.txt b/nikola/data/symlinked.txt index aae8ea1..ce34ddf 100644 --- a/nikola/data/symlinked.txt +++ b/nikola/data/symlinked.txt @@ -14,105 +14,15 @@ nikola/data/samplesite/stories/theming.rst nikola/data/symlink-test-link.txt nikola/data/themes/base/assets/js/moment-with-locales.min.js nikola/data/themes/base/messages/messages_cz.py -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/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-bn.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/jquery.colorbox-min.js -nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js -nikola/data/themes/bootstrap-jinja/assets/js/jquery.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-bn.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.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/colorbox.css +nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png +nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif 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 @@ -120,6 +30,49 @@ nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.wo nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js +nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js +nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js +nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js +nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map 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 @@ -127,6 +80,9 @@ 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/css/colorbox.css +nikola/data/themes/bootstrap3/assets/css/images/controls.png +nikola/data/themes/bootstrap3/assets/css/images/loading.gif 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 @@ -134,4 +90,47 @@ nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 nikola/data/themes/bootstrap3/assets/js/bootstrap.js nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js +nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js +nikola/data/themes/bootstrap3/assets/js/jquery.js +nikola/data/themes/bootstrap3/assets/js/jquery.min.js +nikola/data/themes/bootstrap3/assets/js/jquery.min.map nikola/plugins/command/auto/livereload.js diff --git a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl index 565732c..8b9286e 100644 --- a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl +++ b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl @@ -4,7 +4,7 @@ {% block extra_head %} {{ super() }} {% if translations|length > 1 and generate_atom %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/atom+xml" title="Atom for the {{ archive_name }} section ({{ language }})" href="{{ _link("archive_atom", archive_name, language) }}"> {% endfor %} {% elif generate_atom %} diff --git a/nikola/data/themes/base-jinja/templates/base.tmpl b/nikola/data/themes/base-jinja/templates/base.tmpl index 00ba9d7..5412326 100644 --- a/nikola/data/themes/base-jinja/templates/base.tmpl +++ b/nikola/data/themes/base-jinja/templates/base.tmpl @@ -14,13 +14,14 @@ <a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a> <div id="container"> {{ header.html_header() }} - <main id="content" role="main"> + <main id="content"> {% block content %}{% endblock %} </main> {{ footer.html_footer() }} </div> + {{ base.late_load_js() }} + {% block extra_js %}{% endblock %} {{ 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 index 7fcf616..2e541a6 100644 --- a/nikola/data/themes/base-jinja/templates/base_footer.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_footer.tmpl @@ -3,7 +3,7 @@ {% macro html_footer() %} {% if content_footer %} - <footer id="footer" role="contentinfo"> + <footer id="footer"> <p>{{ content_footer }}</p> {{ template_hooks['page_footer']() }} </footer> diff --git a/nikola/data/themes/base-jinja/templates/base_header.tmpl b/nikola/data/themes/base-jinja/templates/base_header.tmpl index 9f79e72..092abd8 100644 --- a/nikola/data/themes/base-jinja/templates/base_header.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_header.tmpl @@ -2,7 +2,7 @@ {% import 'base_helper.tmpl' as base with context %} {% macro html_header() %} - <header id="header" role="banner"> + <header id="header"> {{ html_site_title() }} {{ html_translation_header() }} {{ html_navigation_links() }} @@ -28,7 +28,7 @@ {% endmacro %} {% macro html_navigation_links() %} - <nav id="menu" role="navigation"> + <nav id="menu"> <ul> {% for url, text in navigation_links[lang] %} {% if isinstance(url, tuple) %} diff --git a/nikola/data/themes/base-jinja/templates/base_helper.tmpl b/nikola/data/themes/base-jinja/templates/base_helper.tmpl index baa6810..8b4ed97 100644 --- a/nikola/data/themes/base-jinja/templates/base_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_helper.tmpl @@ -21,17 +21,20 @@ dir="rtl" lang="{{ lang }}"> <head> <meta charset="utf-8"> + <base href="{{ abs_link(permalink) }}"> {% if description %} <meta name="description" content="{{ description }}"> {% endif %} <meta name="viewport" content="width=device-width"> - <title>{{ title|e }} | {{ blog_title|e }}</title> + {% if title == blog_title %} + <title>{{ blog_title|e }}</title> + {% else %} + <title>{{ title|e }} | {{ blog_title|e }}</title> + {% endif %} {{ html_stylesheets() }} {{ html_feedlinks() }} - {% if permalink %} - <link rel="canonical" href="{{ abs_link(permalink) }}"> - {% endif %} + <link rel="canonical" href="{{ abs_link(permalink) }}"> {% if favicons %} {% for name, file, size in favicons %} @@ -90,7 +93,7 @@ lang="{{ lang }}"> {{ rss_link }} {% elif generate_rss %} {% if translations|length > 1 %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}"> {% endfor %} {% else %} @@ -99,7 +102,7 @@ lang="{{ lang }}"> {% endif %} {% if generate_atom %} {% if translations|length > 1 %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}"> {% endfor %} {% else %} @@ -110,7 +113,7 @@ lang="{{ lang }}"> {% macro html_translations() %} <ul class="translations"> - {% for langname in translations.keys() %} + {% for langname in translations|sort %} {% if langname != lang %} <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> {% endif %} diff --git a/nikola/data/themes/base-jinja/templates/index.tmpl b/nikola/data/themes/base-jinja/templates/index.tmpl index fd9fcf4..222da43 100644 --- a/nikola/data/themes/base-jinja/templates/index.tmpl +++ b/nikola/data/themes/base-jinja/templates/index.tmpl @@ -19,7 +19,7 @@ <h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title()|e }}</a></h1> <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() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></a></p> + <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format) }}">{{ 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 %} diff --git a/nikola/data/themes/base-jinja/templates/list_post.tmpl b/nikola/data/themes/base-jinja/templates/list_post.tmpl index 166d8c4..919d6f0 100644 --- a/nikola/data/themes/base-jinja/templates/list_post.tmpl +++ b/nikola/data/themes/base-jinja/templates/list_post.tmpl @@ -9,7 +9,7 @@ {% if posts %} <ul class="postlist"> {% for post in posts %} - <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li> + <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li> {% endfor %} </ul> {% else %} diff --git a/nikola/data/themes/base-jinja/templates/post_header.tmpl b/nikola/data/themes/base-jinja/templates/post_header.tmpl index 00b6210..dc844e3 100644 --- a/nikola/data/themes/base-jinja/templates/post_header.tmpl +++ b/nikola/data/themes/base-jinja/templates/post_header.tmpl @@ -12,7 +12,7 @@ {% if post.translated_to|length > 1 %} <div class="metadata posttranslations translations"> <h3 class="posttranslations-intro">{{ messages("Also available in:") }}</h3> - {% for langname in translations.keys() %} + {% for langname in translations|sort %} {% 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 %} @@ -32,7 +32,7 @@ {{ 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="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></a></p> + <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" itemprop="datePublished" title="{{ post.formatted_date(date_format) }}">{{ 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 %} diff --git a/nikola/data/themes/base-jinja/templates/post_helper.tmpl b/nikola/data/themes/base-jinja/templates/post_helper.tmpl index ae0206b..b4826eb 100644 --- a/nikola/data/themes/base-jinja/templates/post_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/post_helper.tmpl @@ -2,7 +2,7 @@ {% macro meta_translations(post) %} {% if translations|length > 1 %} - {% for langname in translations.keys() %} + {% for langname in translations|sort %} {% if langname != lang and post.is_translation_available(langname) %} <link rel="alternate" hreflang="{{ langname }}" href="{{ post.permalink(langname) }}"> {% endif %} @@ -58,7 +58,7 @@ {# <meta property="article:author" content="{{ post.author() }}"> #} {# %endif #} {% if post.date.isoformat() %} - <meta property="article:published_time" content="{{ post.date.isoformat() }}"> + <meta property="article:published_time" content="{{ post.formatted_date('webiso') }}"> {% endif %} {% if post.tags %} {% for tag in post.tags %} diff --git a/nikola/data/themes/base-jinja/templates/tag.tmpl b/nikola/data/themes/base-jinja/templates/tag.tmpl index 765c122..0928a5f 100644 --- a/nikola/data/themes/base-jinja/templates/tag.tmpl +++ b/nikola/data/themes/base-jinja/templates/tag.tmpl @@ -4,7 +4,7 @@ {% block extra_head %} {{ super() }} {% if translations|length > 1 and generate_rss %} - {% for language in translations %} + {% for language in translations|sort %} <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 %} @@ -30,7 +30,7 @@ {% endif %} <div class="metadata"> {% if translations|length > 1 and generate_rss %} - {% for language in translations %} + {% for language in translations|sort %} <p class="feedlink"> <a href="{{ _link(kind + "_rss", tag, language) }}" hreflang="{{ language }}" type="application/rss+xml">{{ messages('RSS feed', language) }} ({{ language }})</a> </p> @@ -43,7 +43,7 @@ {% if posts %} <ul class="postlist"> {% for post in posts %} - <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li> + <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li> {% endfor %} </ul> {% endif %} diff --git a/nikola/data/themes/base-jinja/templates/tagindex.tmpl b/nikola/data/themes/base-jinja/templates/tagindex.tmpl index ee7d8b0..f2d2d7a 100644 --- a/nikola/data/themes/base-jinja/templates/tagindex.tmpl +++ b/nikola/data/themes/base-jinja/templates/tagindex.tmpl @@ -15,7 +15,7 @@ {% block extra_head %} {{ super() }} {% if translations|length > 1 and generate_atom %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag }} section ({{ language }})" href="{{ _link(kind + "_atom", tag, language) }}"> {% endfor %} {% elif generate_atom %} diff --git a/nikola/data/themes/base/assets/css/rst.css b/nikola/data/themes/base/assets/css/rst.css index 6e6de97..0fbab76 100644 --- a/nikola/data/themes/base/assets/css/rst.css +++ b/nikola/data/themes/base/assets/css/rst.css @@ -1,6 +1,6 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 7514 2012-09-14 14:27:12Z milde $ +:Id: $Id: html4css1.css 7614 2013-02-21 15:55:51Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. @@ -35,18 +35,10 @@ a.toc-backref { blockquote.epigraph { margin: 2em 5em ; } -dl.docutils dd { - margin-bottom: 0.5em } - object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { overflow: hidden; } -/* Uncomment (and remove this text!) to get bold-faced definition list terms -dl.docutils dt { - font-weight: bold } -*/ - div.abstract { margin: 2em 5em } @@ -55,23 +47,51 @@ div.abstract p.topic-title { text-align: center } div.admonition, div.attention, div.caution, div.danger, div.error, -div.hint, div.important, div.note, div.tip, div.warning { - margin: 2em ; - border: medium outset ; - padding: 1em } +div.hint, div.important, div.note, div.tip, div.warning, div.sidebar { +/* stolen from Boostrap 3 (.panel .panel-default) */ + margin-bottom: 20px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + padding: 0 15px 15px 15px; +} + +div.attention, div.caution, div.danger, div.error, div.warning { + /* stolen from Boostrap 3 (.panel .panel-danger) */ + border-color: #EBCCD1; +} div.admonition p.admonition-title, div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, -div.tip p.admonition-title { - font-weight: bold ; - font-family: sans-serif } +div.tip p.admonition-title, div.sidebar p.sidebar-title, +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { +/* stolen from Boostrap 3 (.panel .panel-default .panel-heading) */ + font-size: 16px; + color: #333; + background-color: #F5F5F5; + padding: 10px 15px; + margin-left: -15px; + margin-right: -15px; + border-bottom: 1px solid rgba(0, 0, 0, 0); + border-top-left-radius: 3px; + border-top-right-radius: 3px; + color: #333; + background-color: #F5F5F5; + border-color: #DDD; +} div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, -div.warning p.admonition-title, .code .error { - color: red ; - font-weight: bold ; - font-family: sans-serif } +div.warning p.admonition-title { + /* stolen from Boostrap 3 (.panel .panel-danger) */ + color: #A94442; + background-color: #F2DEDE; + border-color: #EBCCD1; +} /* Uncomment (and remove this text!) to get reduced vertical space in compound paragraphs. @@ -118,48 +138,32 @@ html[dir="rtl"] div.line-block div.line-block { } div.sidebar { - margin: 0 0 0.5em 1em ; - border: medium outset ; - padding: 1em ; - background-color: #ffffee ; + margin-left: 2em; + min-height: 20px; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { - font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { - color: red } + color: #a94442 } div.system-message { - border: medium outset ; + border: 1px solid #ebccd1; padding: 1em } div.system-message p.system-message-title { - color: red ; + color: #a94442 ; font-weight: bold } div.topic { margin: 2em } -h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, -h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { - margin-top: 0.4em } - -h1.title { - text-align: center } - -h2.subtitle { - text-align: center } - -hr.docutils { - width: 75% } - img.align-left, .figure.align-left, object.align-left { clear: left ; float: left ; @@ -231,13 +235,7 @@ p.rubric { color: maroon ; text-align: center } -p.sidebar-title { - font-family: sans-serif ; - font-weight: bold ; - font-size: larger } - p.sidebar-subtitle { - font-family: sans-serif ; font-weight: bold } p.topic-title { @@ -248,30 +246,23 @@ pre.address { margin-top: 0 ; font: inherit } -pre.literal-block, pre.doctest-block, pre.math, pre.code { - margin-left: 2em ; - margin-right: 2em } - pre.code .ln { color: grey; } /* line numbers */ -pre.code, code { background-color: #eeeeee; } +/* +pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } pre.code .literal.string, code .literal.string { color: #0C5404 } pre.code .name.builtin, code .name.builtin { color: #352B84 } pre.code .deleted, code .deleted { background-color: #DEB0A1} pre.code .inserted, code .inserted { background-color: #A3D289} +*/ span.classifier { - font-family: sans-serif ; - font-style: oblique } + font-style: italic } span.classifier-delimiter { - font-family: sans-serif ; font-weight: bold } -span.interpreted { - font-family: sans-serif } - span.option { white-space: nowrap } @@ -312,6 +303,21 @@ table.docutils th.field-name, table.docinfo th.docinfo-name { white-space: nowrap ; padding-left: 0 } +/* "booktabs" style (no vertical lines) */ +table.docutils.booktabs { + border: 0px; + border-top: 2px solid; + border-bottom: 2px solid; + border-collapse: collapse; +} +table.docutils.booktabs * { + border: 0px; +} +table.docutils.booktabs th { + border-bottom: thin solid; + text-align: left; +} + h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { font-size: 100% } diff --git a/nikola/data/themes/base/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css index 3cb8628..5c7c86d 100644 --- a/nikola/data/themes/base/assets/css/theme.css +++ b/nikola/data/themes/base/assets/css/theme.css @@ -320,6 +320,7 @@ pre.code, code { */ a[href^="#"]:after, + a[href^="data:"]:after, a[href^="javascript:"]:after { content: ""; } diff --git a/nikola/data/themes/base/messages/messages_bs.py b/nikola/data/themes/base/messages/messages_bs.py new file mode 100644 index 0000000..23a1f64 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_bs.py @@ -0,0 +1,39 @@ +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "%d min remaining to read": "%d minuta preostalo za čitanje", + "(active)": "(aktivno)", + "Also available in:": "Takođe dostupan u:", + "Archive": "Arhiva", + "Categories": "Kategorije", + "Comments": "Komentari", + "LANGUAGE": "Bosanski", + "Languages:": "Jezici:", + "More posts about %s": "Više objava o %s", + "Newer posts": "Novije objave", + "Next post": "Naredna objava", + "No posts found.": "Nema objava.", + "Nothing found.": "Ništa nije pronađeno.", + "Older posts": "Starije objave", + "Original site": "Izvorni sajt", + "Posted:": "Objavljeno:", + "Posts about %s": "Objave o %s", + "Posts for year %s": "Objave u godini %s", + "Posts for {month} {day}, {year}": "Objave za {month} {day}, {year}", + "Posts for {month} {year}": "Objave za {month} {year}", + "Previous post": "Prethodne objave", + "Publication date": "Datum objavljivanja", + "RSS feed": "RSS feed", + "Read in English": "Pročitaj na bosanskom", + "Read more": "Pročitaj više", + "Skip to main content": "Preskoči na glavni sadržaj", + "Source": "Izvor", + "Subcategories:": "Podkategorije:", + "Tags and Categories": "Oznake i kategorije", + "Tags": "Oznake", + "Write your page here.": "Vašu stranicu napišite ovdje.", + "Write your post here.": "Vašu objavu napišite ovdje.", + "old posts, page %d": "stare objave, strana %d", + "page %d": "strana %d", +} diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py index bcc2680..1bcd6ad 100644 --- a/nikola/data/themes/base/messages/messages_fi.py +++ b/nikola/data/themes/base/messages/messages_fi.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals MESSAGES = { "%d min remaining to read": "%d minuuttia lukuaikaa", - "(active)": "", + "(active)": "(aktiivinen)", "Also available in:": "Saatavilla myös:", "Archive": "Arkisto", "Categories": "Kategoriat", @@ -22,14 +22,14 @@ MESSAGES = { "Posts for year %s": "Postauksia vuodelta %s", "Posts for {month} {day}, {year}": "", "Posts for {month} {year}": "Postauksia ajalle {month} {year}", - "Previous post": "Vanhempia postauksia", + "Previous post": "Edellinen postaus", "Publication date": "Julkaisupäivämäärä", - "RSS feed": "RSS syöte", + "RSS feed": "RSS-syöte", "Read in English": "Lue suomeksi", "Read more": "Lue lisää", "Skip to main content": "Hyppää sisältöön", "Source": "Lähde", - "Subcategories:": "", + "Subcategories:": "Alakategoriat:", "Tags and Categories": "Tagit ja kategoriat", "Tags": "Tagit", "Write your page here.": "", diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py index d26fc85..11e07d8 100644 --- a/nikola/data/themes/base/messages/messages_hr.py +++ b/nikola/data/themes/base/messages/messages_hr.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals MESSAGES = { - "%d min remaining to read": "", - "(active)": "", + "%d min remaining to read": "%d minuta preostalo za čitanje", + "(active)": "(aktivno)", "Also available in:": "Također dostupno i u:", "Archive": "Arhiva", "Categories": "Kategorije", @@ -27,9 +27,9 @@ MESSAGES = { "RSS feed": "RSS kanal", "Read in English": "Čitaj na hrvatskom", "Read more": "Čitaj dalje", - "Skip to main content": "", + "Skip to main content": "Preskoči na glavni sadržaj", "Source": "Izvor", - "Subcategories:": "", + "Subcategories:": "Podkategorije:", "Tags and Categories": "Tagovi i kategorije", "Tags": "Tagovi", "Write your page here.": "", diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py index 91f8374..9f6d8be 100644 --- a/nikola/data/themes/base/messages/messages_it.py +++ b/nikola/data/themes/base/messages/messages_it.py @@ -8,7 +8,7 @@ MESSAGES = { "Archive": "Archivio", "Categories": "Categorie", "Comments": "Commenti", - "LANGUAGE": "Inglese", + "LANGUAGE": "Italiano", "Languages:": "Lingue:", "More posts about %s": "Altri articoli collegati %s", "Newer posts": "Articoli più recenti", @@ -30,7 +30,7 @@ MESSAGES = { "Skip to main content": "Vai al testo principale", "Source": "Sorgente", "Subcategories:": "Sottocategorie:", - "Tags and Categories": "Tag e Categorie", + "Tags and Categories": "Tag e categorie", "Tags": "Tag", "Write your page here.": "Scrivi qui la tua pagina.", "Write your post here.": "Scrivi qui il tuo post.", diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py index 2533247..30b798e 100644 --- a/nikola/data/themes/base/messages/messages_nb.py +++ b/nikola/data/themes/base/messages/messages_nb.py @@ -2,38 +2,38 @@ from __future__ import unicode_literals MESSAGES = { - "%d min remaining to read": "", - "(active)": "", + "%d min remaining to read": "%d min gjenstår å lese", + "(active)": "(aktiv)", "Also available in:": "Også tilgjengelig på:", "Archive": "Arkiv", "Categories": "Kategorier", - "Comments": "", + "Comments": "Kommentarer", "LANGUAGE": "norsk", - "Languages:": "", + "Languages:": "Språk:", "More posts about %s": "Flere innlegg om %s", "Newer posts": "Nyere innlegg", "Next post": "Neste innlegg", - "No posts found.": "", - "Nothing found.": "", + "No posts found.": "Fant ingen innlegg.", + "Nothing found.": "Fant ingenting.", "Older posts": "Eldre innlegg", "Original site": "Opprinnelig side", "Posted:": "Publisert:", "Posts about %s": "Innlegg om %s", "Posts for year %s": "Innlegg fra %s", - "Posts for {month} {day}, {year}": "", + "Posts for {month} {day}, {year}": "Innlegg fra {day}. {month} {year}", "Posts for {month} {year}": "Innlegg fra {month} {year}", "Previous post": "Forrige innlegg", - "Publication date": "", - "RSS feed": "", + "Publication date": "Publiseringsdato", + "RSS feed": "RSS-nyhetskanal", "Read in English": "Les på norsk", "Read more": "Les mer", - "Skip to main content": "", + "Skip to main content": "Hopp til hovedinnholdet", "Source": "Kilde", - "Subcategories:": "", + "Subcategories:": "Underkategorier:", "Tags and Categories": "Merker og kategorier", "Tags": "Merker", - "Write your page here.": "", - "Write your post here.": "", + "Write your page here.": "Skriv siden din her.", + "Write your post here.": "Skriv innlegget din her.", "old posts, page %d": "eldre innlegg, side %d", "page %d": "side %d", } diff --git a/nikola/data/themes/base/messages/messages_pa.py b/nikola/data/themes/base/messages/messages_pa.py new file mode 100644 index 0000000..392a38b --- /dev/null +++ b/nikola/data/themes/base/messages/messages_pa.py @@ -0,0 +1,39 @@ +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "%d min remaining to read": "ਪੜਣ ਲਈ %d ਮਿੰਟ ਬਾਕੀ", + "(active)": "(ਚਲੰਤ)", + "Also available in:": "ਹੋਰ ਉਪਲਬਧ ਬੋਲੀਆਂ:", + "Archive": "ਆਰਕਾਈਵ", + "Categories": "ਸ਼੍ਰੇਣੀ", + "Comments": "ਟਿੱਪਣੀਆਂ", + "LANGUAGE": "ਅੰਗਰੇਜ਼ੀ", + "Languages:": "ਬੋਲੀਆਂ:", + "More posts about %s": "%s ਬਾਰੇ ਹੋਰ ਲਿਖਤਾਂ", + "Newer posts": "ਨਵੀਆਂ ਲਿਖਤਾਂ", + "Next post": "ਅਗਲੀ ਲਿਖਤ", + "No posts found.": "ਕੋਈ ਲਿਖਤ ਨਹੀਂ ਲੱਭੀ |", + "Nothing found.": "ਕੁਝ ਨਹੀਂ ਲੱਭਿਆ |", + "Older posts": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ", + "Original site": "ਅਸਲ ਸਾਈਟ", + "Posted:": "ਲਿਖਤ ਛਪੀ:", + "Posts about %s": "%s ਬਾਰੇ ਲਿਖਤਾਂ", + "Posts for year %s": "ਸਾਲ %s ਦੀਆਂ ਲਿਖਤਾਂ", + "Posts for {month} {day}, {year}": "{day} {month} {year} ਦੀਆਂ ਲਿਖਤਾਂ", + "Posts for {month} {year}": "{month} {year} ਦੀਆਂ ਲਿਖਤਾਂ", + "Previous post": "ਪਿਛਲੀ ਲਿਖਤ", + "Publication date": "ਛਪਾਈ ਦੀ ਤਰੀਕ", + "RSS feed": "ਆਰ ਐੱਸ ਐੱਸ ਫੀਡ", + "Read in English": "ਅੰਗਰੇਜ਼ੀ ਵਿੱਚ ਪੜ੍ਹੋ", + "Read more": "ਹੋਰ ਪੜ੍ਹੋ", + "Skip to main content": "ਮੁੱਖ ਸਮੱਗਰੀ ਵੱਲ ਜਾਓ", + "Source": "ਮੂਲ", + "Subcategories:": "ਉਪਸ਼੍ਰੇਣੀਆਂ:", + "Tags and Categories": "ਟੈਗ ਅਤੇ ਸ਼੍ਰੇਣੀਆਂ", + "Tags": "ਟੈਗ", + "Write your page here.": "ਆਪਣਾ ਸਫ਼ਾ ਏਥੇ ਲਿਖੋ |", + "Write your post here.": "ਆਪਣੀ ਲਿਖਤ ਏਥੇ ਲਿਖੋ |", + "old posts, page %d": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ , ਸਫ਼ਾ %d", + "page %d": "ਸਫ਼ਾ %d", +} diff --git a/nikola/data/themes/base/messages/messages_pt.py b/nikola/data/themes/base/messages/messages_pt.py index fd26d77..91ea52e 100644 --- a/nikola/data/themes/base/messages/messages_pt.py +++ b/nikola/data/themes/base/messages/messages_pt.py @@ -2,38 +2,38 @@ from __future__ import unicode_literals MESSAGES = { - "%d min remaining to read": "", - "(active)": "", - "Also available in:": "", - "Archive": "", - "Categories": "", - "Comments": "", - "LANGUAGE": "", - "Languages:": "", - "More posts about %s": "", - "Newer posts": "", - "Next post": "", - "No posts found.": "", - "Nothing found.": "", - "Older posts": "", - "Original site": "", - "Posted:": "", - "Posts about %s": "", - "Posts for year %s": "", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "", - "Previous post": "", - "Publication date": "", - "RSS feed": "", - "Read in English": "", - "Read more": "", - "Skip to main content": "", - "Source": "", - "Subcategories:": "", - "Tags and Categories": "", - "Tags": "", - "Write your page here.": "", - "Write your post here.": "", - "old posts, page %d": "", - "page %d": "", + "%d min remaining to read": "%d minutos restante para leitura", + "(active)": "(ativo)", + "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 textos publicados sobre %s", + "Newer posts": "Textos publicados mais recentes", + "Next post": "Próximo texto publicado", + "No posts found.": "Nenhum texto publicado foi encontrado", + "Nothing found.": "Nada encontrado.", + "Older posts": "Textos publicados mais antigos", + "Original site": "Sítio original", + "Posted:": "Publicado:", + "Posts about %s": "Textos publicados sobre %s", + "Posts for year %s": "Textos publicados do ano %s", + "Posts for {month} {day}, {year}": "Textos publicados de {day} {month} {year}", + "Posts for {month} {year}": "Textos publicados de {month} {year}", + "Previous post": "Texto publicado anterior", + "Publication date": "Data de publicação", + "RSS feed": "Feed RSS", + "Read in English": "Ler em português", + "Read more": "Ler mais", + "Skip to main content": "Saltar para o conteúdo principal", + "Source": "Código", + "Subcategories:": "Sub-Categorias:", + "Tags and Categories": "Etiquetas e Categorias", + "Tags": "Etiqueta", + "Write your page here.": "Escreva a sua página aqui.", + "Write your post here.": "Escreva o seu texto para publicar aqui.", + "old posts, page %d": "Textos publicados antigos, página %d", + "page %d": "página %d", } diff --git a/nikola/data/themes/base/messages/messages_sk.py b/nikola/data/themes/base/messages/messages_sk.py index a793ba6..acd7364 100644 --- a/nikola/data/themes/base/messages/messages_sk.py +++ b/nikola/data/themes/base/messages/messages_sk.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals MESSAGES = { "%d min remaining to read": "zostáva %d minút na čítanie", - "(active)": "", + "(active)": "(aktívne)", "Also available in:": "Tiež dostupné v:", "Archive": "Archív", "Categories": "Kategórie", @@ -20,7 +20,7 @@ MESSAGES = { "Posted:": "Zverejnené:", "Posts about %s": "Príspevky o %s", "Posts for year %s": "Príspevky z roku %s", - "Posts for {month} {day}, {year}": "Príspevky z dňa {day}. {month} {year}", + "Posts for {month} {day}, {year}": "Príspevky zo dňa {day}. {month} {year}", "Posts for {month} {year}": "Príspevky za mesiac {month} z roku {year}", "Previous post": "Predchádzajúci príspevok", "Publication date": "Dátum zverejnenia", @@ -29,11 +29,11 @@ MESSAGES = { "Read more": "Čítať ďalej", "Skip to main content": "Skočiť na hlavný obsah", "Source": "Zdroj", - "Subcategories:": "", + "Subcategories:": "Podkategórie:", "Tags and Categories": "Štítky a kategórie", "Tags": "Štítky", - "Write your page here.": "", - "Write your post here.": "", + "Write your page here.": "Tu napíšte svoju stránku.", + "Write your post here.": "Tu napíšte svoj príspevok.", "old posts, page %d": "staré príspevky, strana %d", "page %d": "stránka %d", } diff --git a/nikola/data/themes/base/messages/messages_sr.py b/nikola/data/themes/base/messages/messages_sr.py index 6087027..2b0aaf5 100644 --- a/nikola/data/themes/base/messages/messages_sr.py +++ b/nikola/data/themes/base/messages/messages_sr.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals MESSAGES = { "%d min remaining to read": "%d минута је преостало за читање", - "(active)": "", + "(active)": "(активно)", "Also available in:": "Такође доступан у:", "Archive": "Архива", "Categories": "Категорије", @@ -20,7 +20,7 @@ MESSAGES = { "Posted:": "Објављено:", "Posts about %s": "Постови о %s", "Posts for year %s": "Постови за годину %s", - "Posts for {month} {day}, {year}": "", + "Posts for {month} {day}, {year}": "Објаве за {month} {day}, {year}", "Posts for {month} {year}": "Постови за {month} {year}", "Previous post": "Претходни пост", "Publication date": "Датум објаве", @@ -29,11 +29,11 @@ MESSAGES = { "Read more": "Прочитај више", "Skip to main content": "Прескочи на главни садржај", "Source": "Извор", - "Subcategories:": "", + "Subcategories:": "Подкатегорије:", "Tags and Categories": "Тагови и категорије", "Tags": "Тагови", - "Write your page here.": "", - "Write your post here.": "", + "Write your page here.": "Вашу страницу напишите овдје.", + "Write your post here.": "Вашу објаву напишите овдје.", "old posts, page %d": "стари постови, страна %d", "page %d": "страна %d", } diff --git a/nikola/data/themes/base/messages/messages_sr@latin.py b/nikola/data/themes/base/messages/messages_sr@latin.py new file mode 100644 index 0000000..23a1f64 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_sr@latin.py @@ -0,0 +1,39 @@ +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "%d min remaining to read": "%d minuta preostalo za čitanje", + "(active)": "(aktivno)", + "Also available in:": "Takođe dostupan u:", + "Archive": "Arhiva", + "Categories": "Kategorije", + "Comments": "Komentari", + "LANGUAGE": "Bosanski", + "Languages:": "Jezici:", + "More posts about %s": "Više objava o %s", + "Newer posts": "Novije objave", + "Next post": "Naredna objava", + "No posts found.": "Nema objava.", + "Nothing found.": "Ništa nije pronađeno.", + "Older posts": "Starije objave", + "Original site": "Izvorni sajt", + "Posted:": "Objavljeno:", + "Posts about %s": "Objave o %s", + "Posts for year %s": "Objave u godini %s", + "Posts for {month} {day}, {year}": "Objave za {month} {day}, {year}", + "Posts for {month} {year}": "Objave za {month} {year}", + "Previous post": "Prethodne objave", + "Publication date": "Datum objavljivanja", + "RSS feed": "RSS feed", + "Read in English": "Pročitaj na bosanskom", + "Read more": "Pročitaj više", + "Skip to main content": "Preskoči na glavni sadržaj", + "Source": "Izvor", + "Subcategories:": "Podkategorije:", + "Tags and Categories": "Oznake i kategorije", + "Tags": "Oznake", + "Write your page here.": "Vašu stranicu napišite ovdje.", + "Write your post here.": "Vašu objavu napišite ovdje.", + "old posts, page %d": "stare objave, strana %d", + "page %d": "strana %d", +} diff --git a/nikola/data/themes/base/messages/messages_sv.py b/nikola/data/themes/base/messages/messages_sv.py index deda21a..52df00d 100644 --- a/nikola/data/themes/base/messages/messages_sv.py +++ b/nikola/data/themes/base/messages/messages_sv.py @@ -3,37 +3,37 @@ from __future__ import unicode_literals MESSAGES = { "%d min remaining to read": "%d minuter kvar att läsa", - "(active)": "", + "(active)": "(aktiv)", "Also available in:": "Även tillgänglig på:", "Archive": "Arkiv", "Categories": "Kategorier", "Comments": "Kommentarer", "LANGUAGE": "Svenska", "Languages:": "Språk:", - "More posts about %s": "Mer inlägg om %s", + "More posts about %s": "Fler inlägg om %s", "Newer posts": "Nya inlägg", "Next post": "Nästa inlägg", - "No posts found.": "Inga inlägg hittade", - "Nothing found.": "Inget hittat", + "No posts found.": "Inga inlägg hittade.", + "Nothing found.": "Inget hittat.", "Older posts": "Äldre inlägg", - "Original site": "Orgnialsida", - "Posted:": "Publicerad", + "Original site": "Originalsida", + "Posted:": "Publicerad:", "Posts about %s": "Inlägg om %s", "Posts for year %s": "Inlägg för år %s", "Posts for {month} {day}, {year}": "Inlägg för {month} {day}, {year}", "Posts for {month} {year}": "Inlägg för {month} {year}", "Previous post": "Föregående inlägg", "Publication date": "Publiceringsdatum", - "RSS feed": "RSS flöde", - "Read in English": "Läs på Svenska", + "RSS feed": "RSS-flöde", + "Read in English": "Läs på svenska", "Read more": "Läs mer", - "Skip to main content": "hoppa till huvudinehåll", + "Skip to main content": "Hoppa till huvudinnehåll", "Source": "Källa", "Subcategories:": "Underkategorier:", "Tags and Categories": "Taggar och Kategorier", "Tags": "Taggar", - "Write your page here.": "", - "Write your post here.": "", + "Write your page here.": "Skriv din sida här.", + "Write your post here.": "Skriv ditt inlägg här.", "old posts, page %d": "gamla inlägg, sida %d", "page %d": "sida %d", } diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py index 075606c..074cb06 100644 --- a/nikola/data/themes/base/messages/messages_ur.py +++ b/nikola/data/themes/base/messages/messages_ur.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals MESSAGES = { "%d min remaining to read": "%d منٹ کا مطالعہ باقی", - "(active)": "", + "(active)": "(فعال)", "Also available in:": "ان زبانوں میں بھی دستیاب:", "Archive": "آرکائیو", "Categories": "زمرے", @@ -32,8 +32,8 @@ MESSAGES = { "Subcategories:": "ذیلی زمرے", "Tags and Categories": "ٹیگز اور زمرے", "Tags": "ٹیگز", - "Write your page here.": "", - "Write your post here.": "", + "Write your page here.": "اپنے صفحے کا متن یہاں لکھیں۔", + "Write your post here.": "اپنی تحریر یہاں لکھیں۔", "old posts, page %d": "پرانی تحاریر صفحہ %d", "page %d": "صفحہ %d", } diff --git a/nikola/data/themes/base/templates/archiveindex.tmpl b/nikola/data/themes/base/templates/archiveindex.tmpl index 129b7c4..8c58f13 100644 --- a/nikola/data/themes/base/templates/archiveindex.tmpl +++ b/nikola/data/themes/base/templates/archiveindex.tmpl @@ -4,7 +4,7 @@ <%block name="extra_head"> ${parent.extra_head()} %if len(translations) > 1 and generate_atom: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/atom+xml" title="Atom for the ${archive_name} section (${language})" href="${_link("archive_atom", archive_name, language)}"> %endfor %elif generate_atom: diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl index 6da6416..2b0cbfd 100644 --- a/nikola/data/themes/base/templates/base.tmpl +++ b/nikola/data/themes/base/templates/base.tmpl @@ -14,13 +14,14 @@ ${template_hooks['extra_head']()} <a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a> <div id="container"> ${header.html_header()} - <main id="content" role="main"> + <main id="content"> <%block name="content"></%block> </main> ${footer.html_footer()} </div> + ${base.late_load_js()} + <%block name="extra_js"></%block> ${body_end} ${template_hooks['body_end']()} - ${base.late_load_js()} </body> </html> diff --git a/nikola/data/themes/base/templates/base_footer.tmpl b/nikola/data/themes/base/templates/base_footer.tmpl index 9a1c00f..cd41d37 100644 --- a/nikola/data/themes/base/templates/base_footer.tmpl +++ b/nikola/data/themes/base/templates/base_footer.tmpl @@ -3,7 +3,7 @@ <%def name="html_footer()"> %if content_footer: - <footer id="footer" role="contentinfo"> + <footer id="footer"> <p>${content_footer}</p> ${template_hooks['page_footer']()} </footer> diff --git a/nikola/data/themes/base/templates/base_header.tmpl b/nikola/data/themes/base/templates/base_header.tmpl index 0c19af6..e29e2b3 100644 --- a/nikola/data/themes/base/templates/base_header.tmpl +++ b/nikola/data/themes/base/templates/base_header.tmpl @@ -2,7 +2,7 @@ <%namespace name="base" file="base_helper.tmpl" import="*"/> <%def name="html_header()"> - <header id="header" role="banner"> + <header id="header"> ${html_site_title()} ${html_translation_header()} ${html_navigation_links()} @@ -28,7 +28,7 @@ </%def> <%def name="html_navigation_links()"> - <nav id="menu" role="navigation"> + <nav id="menu"> <ul> %for url, text in navigation_links[lang]: % if isinstance(url, tuple): diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl index 28b3f8a..948cfba 100644 --- a/nikola/data/themes/base/templates/base_helper.tmpl +++ b/nikola/data/themes/base/templates/base_helper.tmpl @@ -21,17 +21,20 @@ dir="rtl" \ lang="${lang}"> <head> <meta charset="utf-8"> + <base href="${abs_link(permalink)}"> %if description: <meta name="description" content="${description}"> %endif <meta name="viewport" content="width=device-width"> - <title>${title|striphtml} | ${blog_title|striphtml}</title> + %if title == blog_title: + <title>${blog_title|h}</title> + %else: + <title>${title|h} | ${blog_title|h}</title> + %endif ${html_stylesheets()} ${html_feedlinks()} - %if permalink: - <link rel="canonical" href="${abs_link(permalink)}"> - %endif + <link rel="canonical" href="${abs_link(permalink)}"> %if favicons: %for name, file, size in favicons: @@ -90,7 +93,7 @@ lang="${lang}"> ${rss_link} %elif generate_rss: %if len(translations) > 1: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}"> %endfor %else: @@ -99,7 +102,7 @@ lang="${lang}"> %endif %if generate_atom: %if len(translations) > 1: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}"> %endfor %else: @@ -110,7 +113,7 @@ lang="${lang}"> <%def name="html_translations()"> <ul class="translations"> - %for langname in translations.keys(): + %for langname in sorted(translations): %if langname != lang: <li><a href="${abs_link(_link("root", None, langname))}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li> %endif diff --git a/nikola/data/themes/base/templates/index.tmpl b/nikola/data/themes/base/templates/index.tmpl index 69630e5..88bb25c 100644 --- a/nikola/data/themes/base/templates/index.tmpl +++ b/nikola/data/themes/base/templates/index.tmpl @@ -19,7 +19,7 @@ <h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()|h}</a></h1> <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()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></a></p> + <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)}">${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 diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl index 4fb497c..3335fa6 100644 --- a/nikola/data/themes/base/templates/list_post.tmpl +++ b/nikola/data/themes/base/templates/list_post.tmpl @@ -9,7 +9,7 @@ %if posts: <ul class="postlist"> % for post in posts: - <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li> + <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li> % endfor </ul> %else: diff --git a/nikola/data/themes/base/templates/post_header.tmpl b/nikola/data/themes/base/templates/post_header.tmpl index 0efb2f8..75383cb 100644 --- a/nikola/data/themes/base/templates/post_header.tmpl +++ b/nikola/data/themes/base/templates/post_header.tmpl @@ -12,7 +12,7 @@ % if len(post.translated_to) > 1: <div class="metadata posttranslations translations"> <h3 class="posttranslations-intro">${messages("Also available in:")}</h3> - % for langname in translations.keys(): + % for langname in sorted(translations): % 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 @@ -32,7 +32,7 @@ ${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="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></a></p> + <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.formatted_date('webiso')}" itemprop="datePublished" title="${post.formatted_date(date_format)}">${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 diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl index e091447..424d134 100644 --- a/nikola/data/themes/base/templates/post_helper.tmpl +++ b/nikola/data/themes/base/templates/post_helper.tmpl @@ -2,7 +2,7 @@ <%def name="meta_translations(post)"> %if len(translations) > 1: - %for langname in translations.keys(): + %for langname in sorted(translations): %if langname != lang and post.is_translation_available(langname): <link rel="alternate" hreflang="${langname}" href="${post.permalink(langname)}"> %endif @@ -58,7 +58,7 @@ ### <meta property="article:author" content="${post.author()}"> ### %endif %if post.date.isoformat(): - <meta property="article:published_time" content="${post.date.isoformat()}"> + <meta property="article:published_time" content="${post.formatted_date('webiso')}"> %endif %if post.tags: %for tag in post.tags: diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl index ec5caca..7509f3e 100644 --- a/nikola/data/themes/base/templates/tag.tmpl +++ b/nikola/data/themes/base/templates/tag.tmpl @@ -4,7 +4,7 @@ <%block name="extra_head"> ${parent.extra_head()} %if len(translations) > 1 and generate_rss: - %for language in translations: + %for language in sorted(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: @@ -30,7 +30,7 @@ %endif <div class="metadata"> %if len(translations) > 1 and generate_rss: - %for language in translations: + %for language in sorted(translations): <p class="feedlink"> <a href="${_link(kind + "_rss", tag, language)}" hreflang="${language}" type="application/rss+xml">${messages('RSS feed', language)} (${language})</a> </p> @@ -43,7 +43,7 @@ %if posts: <ul class="postlist"> % for post in posts: - <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li> + <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li> % endfor </ul> %endif diff --git a/nikola/data/themes/base/templates/tagindex.tmpl b/nikola/data/themes/base/templates/tagindex.tmpl index 7160fe9..0834411 100644 --- a/nikola/data/themes/base/templates/tagindex.tmpl +++ b/nikola/data/themes/base/templates/tagindex.tmpl @@ -15,7 +15,7 @@ <%block name="extra_head"> ${parent.extra_head()} %if len(translations) > 1 and generate_atom: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag} section (${language})" href="${_link(kind + "_atom", tag, language)}"> %endfor %elif generate_atom: diff --git a/nikola/data/themes/bootstrap-jinja/AUTHORS.txt b/nikola/data/themes/bootstrap-jinja/AUTHORS.txt deleted file mode 100644 index 043d497..0000000 --- a/nikola/data/themes/bootstrap-jinja/AUTHORS.txt +++ /dev/null @@ -1 +0,0 @@ -Roberto Alsina <https://github.com/ralsina> diff --git a/nikola/data/themes/bootstrap-jinja/README.md b/nikola/data/themes/bootstrap-jinja/README.md deleted file mode 100644 index 637caf5..0000000 --- a/nikola/data/themes/bootstrap-jinja/README.md +++ /dev/null @@ -1,23 +0,0 @@ -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: https://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/theme.css b/nikola/data/themes/bootstrap-jinja/assets/css/theme.css deleted file mode 100644 index 4fc31a8..0000000 --- a/nikola/data/themes/bootstrap-jinja/assets/css/theme.css +++ /dev/null @@ -1,205 +0,0 @@ -#container { - width: 960px; - margin: 50 auto; -} - -#contentcolumn { - max-width: 760px; -} -#q { - width: 150px; -} - -img { - max-width: 90%; -} - -.postbox { - border-bottom: 2px solid darkgrey; - margin-bottom: 12px; -} - -.footerbox {padding: 15px; text-align: center; margin-bottom: 15px;} - -td.label { - /* Issue #290 */ - background-color: inherit; -} - -.footnote-reference { - /* Issue 290 */ - vertical-align: super; - font-size: xx-small; -} - -.caption { - /* Issue 292 */ - text-align: center; - padding-top: 1em; -} - -div.figure > img, -div.figure > a > img { - /* Issue 292 */ - display: block; - margin-left: auto; - margin-right: auto; -} - -div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { - /* Issue 277 */ - border: 1px solid #aaa; - border-radius: 5px; -} - -blockquote p, blockquote { - font-size: 17.5px; - font-weight: 300; - line-height: 1.25; -} - -ul.bricks > li { - display: inline; - background-color: lightblue; - padding: 8px; - border-radius: 5px; - line-height: 3; - white-space:nowrap; - margin: 3px; -} - -ul.breadcrumb > li:before { - content: " / "; -} - -pre, pre code { - white-space: pre; - word-wrap: normal; - overflow: auto; -} - -article.post-micro { - font-family: Georgia, 'Times New Roman', Times, serif; - font-size: 1.5em; -} - -.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; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} - -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} - -/* hat tip bootstrap/html5 boilerplate */ -@media print { - *, *:before, *:after { - font-family: Garamond, Junicode, serif; - } - - body { - font-size: 12pt; - } - - article .entry-title a[href]:after, - article .metadata a[href]:after, - article .tags a[href]:after { - content: ""; - } - - article .metadata .sourceline { - display: none; - } - - article .metadata .linkline a[href]:after { - content: " (" attr(href) ")"; - } - - .navbar { - display: none; - } -} diff --git a/nikola/data/themes/bootstrap-jinja/bundles b/nikola/data/themes/bootstrap-jinja/bundles deleted file mode 120000 index 3e517bb..0000000 --- a/nikola/data/themes/bootstrap-jinja/bundles +++ /dev/null @@ -1 +0,0 @@ -../bootstrap/bundles
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap-jinja/engine b/nikola/data/themes/bootstrap-jinja/engine deleted file mode 100644 index 6f04b30..0000000 --- a/nikola/data/themes/bootstrap-jinja/engine +++ /dev/null @@ -1 +0,0 @@ -jinja diff --git a/nikola/data/themes/bootstrap-jinja/parent b/nikola/data/themes/bootstrap-jinja/parent deleted file mode 100644 index e9ed660..0000000 --- a/nikola/data/themes/bootstrap-jinja/parent +++ /dev/null @@ -1 +0,0 @@ -base-jinja diff --git a/nikola/data/themes/bootstrap-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap-jinja/templates/base.tmpl deleted file mode 100644 index 3177276..0000000 --- a/nikola/data/themes/bootstrap-jinja/templates/base.tmpl +++ /dev/null @@ -1,93 +0,0 @@ -{# -*- 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> -<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a> - -<!-- Menubar --> - -<div class="navbar navbar-static-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(_link("root", None, lang)) }}"> - {% 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="content" role="main"> - <!--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>$('a.image-reference:not(.islink) img:not(.islink)').parent().colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script> - <!-- fancy dates --> - <script> - moment.locale("{{ momentjs_locales[lang] }}"); - fancydates({{ date_fanciness }}, {{ js_date_format }}); - </script> - <!-- end fancy dates --> - {% 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 deleted file mode 100644 index 4a29e69..0000000 --- a/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl +++ /dev/null @@ -1,184 +0,0 @@ -{# -*- coding: utf-8 -*- #} - -{% import 'annotation_helper.tmpl' as notes with context %} -{% 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 %} - - {% if prevlink %} - <link rel="prev" href="{{ prevlink }}" type="text/html"> - {% endif %} - {% if nextlink %} - <link rel="next" href="{{ nextlink }}" type="text/html"> - {% 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="{{ url_replacer(permalink, '/assets/js/html5.js', lang) }}"></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.3/jquery.min.js"></script> - <script src="//maxcdn.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.3/jquery.min.js"></script> - <script src="//maxcdn.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> - <script src="/assets/js/moment-with-locales.min.js"></script> - <script src="/assets/js/fancydates.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="//maxcdn.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="//maxcdn.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 needs_ipython_css %} - <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css"> - {% 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 isinstance(url, tuple) %} - <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 }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a> - {% else %} - <li><a href="{{ suburl }}">{{ text }}</a> - {% endif %} - {% endfor %} - </ul> - {% else %} - {% if rel_link(permalink, url) == "#" %} - <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></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 %} - {% if generate_atom %} - {% if translations|length > 1 %} - {% for language in translations %} - <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}"> - {% endfor %} - {% else %} - <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}"> - {% endif %} - {% endif %} -{% endmacro %} - -{% macro html_translations() %} - {% for langname in translations.keys() %} - {% if langname != lang %} - <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> - {% endif %} - {% endfor %} -{% endmacro %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl b/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl deleted file mode 100644 index 07112da..0000000 --- a/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl +++ /dev/null @@ -1,94 +0,0 @@ -{# -*- 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|e }}</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() }} -<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> -<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 deleted file mode 100644 index 4b99f86..0000000 --- a/nikola/data/themes/bootstrap-jinja/templates/listing.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -{# -*- 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/slides.tmpl b/nikola/data/themes/bootstrap-jinja/templates/slides.tmpl deleted file mode 100644 index 0ae8fe8..0000000 --- a/nikola/data/themes/bootstrap-jinja/templates/slides.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{% 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/README.md b/nikola/data/themes/bootstrap/README.md deleted file mode 100644 index 637caf5..0000000 --- a/nikola/data/themes/bootstrap/README.md +++ /dev/null @@ -1,23 +0,0 @@ -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: https://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/assets/css/theme.css b/nikola/data/themes/bootstrap/assets/css/theme.css deleted file mode 100644 index 4fc31a8..0000000 --- a/nikola/data/themes/bootstrap/assets/css/theme.css +++ /dev/null @@ -1,205 +0,0 @@ -#container { - width: 960px; - margin: 50 auto; -} - -#contentcolumn { - max-width: 760px; -} -#q { - width: 150px; -} - -img { - max-width: 90%; -} - -.postbox { - border-bottom: 2px solid darkgrey; - margin-bottom: 12px; -} - -.footerbox {padding: 15px; text-align: center; margin-bottom: 15px;} - -td.label { - /* Issue #290 */ - background-color: inherit; -} - -.footnote-reference { - /* Issue 290 */ - vertical-align: super; - font-size: xx-small; -} - -.caption { - /* Issue 292 */ - text-align: center; - padding-top: 1em; -} - -div.figure > img, -div.figure > a > img { - /* Issue 292 */ - display: block; - margin-left: auto; - margin-right: auto; -} - -div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { - /* Issue 277 */ - border: 1px solid #aaa; - border-radius: 5px; -} - -blockquote p, blockquote { - font-size: 17.5px; - font-weight: 300; - line-height: 1.25; -} - -ul.bricks > li { - display: inline; - background-color: lightblue; - padding: 8px; - border-radius: 5px; - line-height: 3; - white-space:nowrap; - margin: 3px; -} - -ul.breadcrumb > li:before { - content: " / "; -} - -pre, pre code { - white-space: pre; - word-wrap: normal; - overflow: auto; -} - -article.post-micro { - font-family: Georgia, 'Times New Roman', Times, serif; - font-size: 1.5em; -} - -.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; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} - -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} - -/* hat tip bootstrap/html5 boilerplate */ -@media print { - *, *:before, *:after { - font-family: Garamond, Junicode, serif; - } - - body { - font-size: 12pt; - } - - article .entry-title a[href]:after, - article .metadata a[href]:after, - article .tags a[href]:after { - content: ""; - } - - article .metadata .sourceline { - display: none; - } - - article .metadata .linkline a[href]:after { - content: " (" attr(href) ")"; - } - - .navbar { - display: none; - } -} diff --git a/nikola/data/themes/bootstrap/bundles b/nikola/data/themes/bootstrap/bundles deleted file mode 100644 index 38257d2..0000000 --- a/nikola/data/themes/bootstrap/bundles +++ /dev/null @@ -1,4 +0,0 @@ -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.min.js,bootstrap.min.js,jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js -assets/js/all.js=jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js diff --git a/nikola/data/themes/bootstrap/engine b/nikola/data/themes/bootstrap/engine deleted file mode 100644 index 2951cdd..0000000 --- a/nikola/data/themes/bootstrap/engine +++ /dev/null @@ -1 +0,0 @@ -mako diff --git a/nikola/data/themes/bootstrap/parent b/nikola/data/themes/bootstrap/parent deleted file mode 100644 index df967b9..0000000 --- a/nikola/data/themes/bootstrap/parent +++ /dev/null @@ -1 +0,0 @@ -base diff --git a/nikola/data/themes/bootstrap/templates/base.tmpl b/nikola/data/themes/bootstrap/templates/base.tmpl deleted file mode 100644 index e848673..0000000 --- a/nikola/data/themes/bootstrap/templates/base.tmpl +++ /dev/null @@ -1,93 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace name="base" file="base_helper.tmpl" import="*" /> -<%namespace name="notes" file="annotation_helper.tmpl" import="*" /> -${set_locale(lang)} -${base.html_headstart()} -<%block name="extra_head"> -### Leave this block alone. -</%block> -${template_hooks['extra_head']()} -</head> -<body> -<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a> - -<!-- Menubar --> - -<div class="navbar navbar-static-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(_link("root", None, lang))}"> - %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 name="belowtitle"> - %if len(translations) > 1: - <li>${base.html_translations()}</li> - %endif - </%block> - % if show_sourcelink: - <li><%block name="sourcelink"></%block></li> - %endif - ${template_hooks['menu_alt']()} - </ul> - </div> - </div> - </div> -</div> -<!-- End of Menubar --> -<div class="container-fluid" id="content" role="main"> - <!--Body content--> - <div class="row-fluid"> - <div class="span2"></div> - <div class="span8"> - ${template_hooks['page_header']()} - <%block name="content"></%block> - </div> - </div> - <!--End of body content--> -</div> -<div class="footerbox"> - ${content_footer} - ${template_hooks['page_footer']()} -</div> -${base.late_load_js()} - <script>$('a.image-reference:not(.islink) img:not(.islink)').parent().colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script> - <!-- fancy dates --> - <script> - moment.locale("${momentjs_locales[lang]}"); - fancydates(${date_fanciness}, ${js_date_format}); - </script> - <!-- end fancy dates --> - <%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} -${template_hooks['body_end']()} -</body> -</html> diff --git a/nikola/data/themes/bootstrap/templates/base_helper.tmpl b/nikola/data/themes/bootstrap/templates/base_helper.tmpl deleted file mode 100644 index 4c62f8d..0000000 --- a/nikola/data/themes/bootstrap/templates/base_helper.tmpl +++ /dev/null @@ -1,184 +0,0 @@ -## -*- coding: utf-8 -*- - -<%namespace name="notes" file="annotation_helper.tmpl" import="*" /> -<%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="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 - - %if prevlink: - <link rel="prev" href="${prevlink}" type="text/html"> - %endif - %if nextlink: - <link rel="next" href="${nextlink}" type="text/html"> - %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="${url_replacer(permalink, '/assets/js/html5.js', lang)}"></script><![endif]--> - %endif - - ${extra_head_data} -</%def> - - -<%def name="late_load_js()"> - %if use_bundles: - %if use_cdn: - <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> - <script src="//maxcdn.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.3/jquery.min.js"></script> - <script src="//maxcdn.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> - <script src="/assets/js/moment-with-locales.min.js"></script> - <script src="/assets/js/fancydates.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} -</%def> - - -<%def name="html_stylesheets()"> - %if use_bundles: - %if use_cdn: - <link href="//maxcdn.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="//maxcdn.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 needs_ipython_css: - <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css"> - <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css"> - % 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> - - -<%def name="html_navigation_links()"> - %for url, text in navigation_links[lang]: - % if isinstance(url, tuple): - <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} <span class="sr-only">${messages("(active)", lang)}</span></a> - %else: - <li><a href="${suburl}">${text}</a> - %endif - %endfor - </ul> - % else: - % if rel_link(permalink, url) == "#": - <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a> - %else: - <li><a href="${url}">${text}</a> - %endif - % 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 - %if generate_atom: - %if len(translations) > 1: - %for language in translations: - <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}"> - %endfor - %else: - <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}"> - %endif - %endif -</%def> - -<%def name="html_translations()"> - %for langname in translations.keys(): - %if langname != lang: - <li><a href="${abs_link(_link("root", 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 deleted file mode 100644 index ab15587..0000000 --- a/nikola/data/themes/bootstrap/templates/gallery.tmpl +++ /dev/null @@ -1,94 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="base.tmpl"/> -<%namespace name="comments" file="comments_helper.tmpl"/> -<%namespace name="ui" file="crumbs.tmpl" import="bar"/> -<%block name="sourcelink"></%block> - -<%block name="content"> - ${ui.bar(crumbs)} - %if title: - <h1>${title|h}</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 -</%block> - -<%block name="extra_head"> -${parent.extra_head()} -<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> -<style type="text/css"> - .image-block { - display: inline-block; - } - .flowr_row { - width: 100%; - } - </style> -</%block> - - -<%block name="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> -</%block> diff --git a/nikola/data/themes/bootstrap/templates/listing.tmpl b/nikola/data/themes/bootstrap/templates/listing.tmpl deleted file mode 100644 index f03ea23..0000000 --- a/nikola/data/themes/bootstrap/templates/listing.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -## -*- 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/slides.tmpl b/nikola/data/themes/bootstrap/templates/slides.tmpl deleted file mode 100644 index 048fb7e..0000000 --- a/nikola/data/themes/bootstrap/templates/slides.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -<%block name="content"> -<div id="${carousel_id}" class="carousel slide"> - <ol class="carousel-indicators"> - % for i in range(len(slides_content)): - % 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> -</%block> diff --git a/nikola/data/themes/bootstrap3-jinja/README.md b/nikola/data/themes/bootstrap3-jinja/README.md index f008daf..10e673a 100644 --- a/nikola/data/themes/bootstrap3-jinja/README.md +++ b/nikola/data/themes/bootstrap3-jinja/README.md @@ -1,4 +1,4 @@ -A bootstrap3 version of the bootstrap theme. +A theme based on Bootstrap 3. There is a variant called bootstrap3-gradients which uses an extra CSS file for a *visually enhanced experience* (according to Bootstrap diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css b/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css index 5f8b3b0..5f8b3b0 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css +++ b/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png index 841a726..841a726 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png +++ b/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif index b192a75..b192a75 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif +++ b/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css index 9ce6472..6964ec6 100644 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css +++ b/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css @@ -51,12 +51,6 @@ div.figure > a > img { margin-right: auto; } -div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { - /* Issue 277 */ - border: 1px solid #aaa; - border-radius: 5px; -} - blockquote p, blockquote { font-size: 17.5px; font-weight: 300; diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js index f83073f..f83073f 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js index bafc4e0..bafc4e0 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js index 9b995d8..9b995d8 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js index a749232..a749232 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js index e4a595c..e4a595c 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js index 1e9a1d6..1e9a1d6 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js index 748f53b..748f53b 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js index 1154fb5..1154fb5 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js index 483e192..483e192 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js index a30b13c..a30b13c 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js index 2a7e8ad..2a7e8ad 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js index e359290..e359290 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js index 04fa276..04fa276 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js index d8105ab..d8105ab 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js index 72dddf5..72dddf5 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js index 34aa3c0..34aa3c0 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js index a87f03c..a87f03c 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js index 31053b8..31053b8 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js index aad9d22..aad9d22 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js index 3ea27c2..3ea27c2 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js index 3e23b4a..3e23b4a 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js index 374b9bb..374b9bb 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js index 101b476..101b476 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js index 8e14f15..8e14f15 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js index 2d03d48..2d03d48 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js index 9af0ba7..9af0ba7 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js index 34f8ab1..34f8ab1 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js index e20bd38..e20bd38 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js index 555f2e6..555f2e6 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js index bac4855..bac4855 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js index 65b0492..65b0492 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js index 99859fd..99859fd 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js index c4fd9d5..c4fd9d5 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js index d7f26e0..d7f26e0 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js index 86fd98f..86fd98f 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js index 7cd1336..7cd1336 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js index e6c5965..e6c5965 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js index bd2254c..bd2254c 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js index c0d986b..c0d986b 100644 --- a/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js index 5ee7a90..5ee7a90 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/jquery.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js index 966173b..966173b 120000 --- a/nikola/data/themes/bootstrap-jinja/assets/js/jquery.js +++ b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js diff --git a/nikola/data/themes/bootstrap3-jinja/parent b/nikola/data/themes/bootstrap3-jinja/parent index e89c4ee..e9ed660 100644 --- a/nikola/data/themes/bootstrap3-jinja/parent +++ b/nikola/data/themes/bootstrap3-jinja/parent @@ -1 +1 @@ -bootstrap-jinja +base-jinja diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl index 058640a..46d145a 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl +++ b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl @@ -13,10 +13,10 @@ <!-- Menubar --> -<nav class="navbar navbar-inverse navbar-static-top" role="navigation"> +<nav class="navbar navbar-inverse navbar-static-top"> <div class="container"><!-- This keeps the margins nice --> <div class="navbar-header"> - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> @@ -32,7 +32,7 @@ {% endif %} </a> </div><!-- /.navbar-header --> - <div class="collapse navbar-collapse navbar-ex1-collapse"> + <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false"> <ul class="nav navbar-nav"> {{ base.html_navigation_links() }} {{ template_hooks['menu']() }} diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl index a481632..5ab4dcb 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl +++ b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl @@ -26,17 +26,20 @@ dir="rtl" lang="{{ lang }}"> <head> <meta charset="utf-8"> + <base href="{{ abs_link(permalink) }}"> {% if description %} <meta name="description" content="{{ description }}"> {% endif %} <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>{{ title|e }} | {{ blog_title|e }}</title> + {% if title == blog_title %} + <title>{{ blog_title|e }}</title> + {% else %} + <title>{{ title|e }} | {{ blog_title|e }}</title> + {% endif %} {{ html_stylesheets() }} {{ html_feedlinks() }} - {% if permalink %} - <link rel="canonical" href="{{ abs_link(permalink) }}"> - {% endif %} + <link rel="canonical" href="{{ abs_link(permalink) }}"> {% if favicons %} {% for name, file, size in favicons %} @@ -129,7 +132,7 @@ lang="{{ lang }}"> {% macro html_navigation_links() %} {% for url, text in navigation_links[lang] %} {% if isinstance(url, tuple) %} - <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }} <b class="caret"></b></a> + <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }} <b class="caret"></b></a> <ul class="dropdown-menu"> {% for suburl, text in url %} {% if rel_link(permalink, suburl) == "#" %} @@ -154,7 +157,7 @@ lang="{{ lang }}"> {{ rss_link }} {% elif generate_rss %} {% if translations|length > 1 %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}"> {% endfor %} {% else %} @@ -163,7 +166,7 @@ lang="{{ lang }}"> {% endif %} {% if generate_atom %} {% if translations|length > 1 %} - {% for language in translations %} + {% for language in translations|sort %} <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}"> {% endfor %} {% else %} @@ -173,7 +176,7 @@ lang="{{ lang }}"> {% endmacro %} {% macro html_translations() %} - {% for langname in translations.keys() %} + {% for langname in translations|sort %} {% if langname != lang %} <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> {% endif %} diff --git a/nikola/data/themes/bootstrap-jinja/templates/post.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl index df4109c..df4109c 100644 --- a/nikola/data/themes/bootstrap-jinja/templates/post.tmpl +++ b/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl diff --git a/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl index c308f19..c308f19 100644 --- a/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl +++ b/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl diff --git a/nikola/data/themes/bootstrap3/README.md b/nikola/data/themes/bootstrap3/README.md index f008daf..10e673a 100644 --- a/nikola/data/themes/bootstrap3/README.md +++ b/nikola/data/themes/bootstrap3/README.md @@ -1,4 +1,4 @@ -A bootstrap3 version of the bootstrap theme. +A theme based on Bootstrap 3. There is a variant called bootstrap3-gradients which uses an extra CSS file for a *visually enhanced experience* (according to Bootstrap diff --git a/nikola/data/themes/bootstrap/assets/css/colorbox.css b/nikola/data/themes/bootstrap3/assets/css/colorbox.css index 5f8b3b0..5f8b3b0 120000 --- a/nikola/data/themes/bootstrap/assets/css/colorbox.css +++ b/nikola/data/themes/bootstrap3/assets/css/colorbox.css diff --git a/nikola/data/themes/bootstrap/assets/css/images/controls.png b/nikola/data/themes/bootstrap3/assets/css/images/controls.png index 841a726..841a726 120000 --- a/nikola/data/themes/bootstrap/assets/css/images/controls.png +++ b/nikola/data/themes/bootstrap3/assets/css/images/controls.png diff --git a/nikola/data/themes/bootstrap/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3/assets/css/images/loading.gif index b192a75..b192a75 120000 --- a/nikola/data/themes/bootstrap/assets/css/images/loading.gif +++ b/nikola/data/themes/bootstrap3/assets/css/images/loading.gif diff --git a/nikola/data/themes/bootstrap3/assets/css/theme.css b/nikola/data/themes/bootstrap3/assets/css/theme.css index 9ce6472..6964ec6 100644 --- a/nikola/data/themes/bootstrap3/assets/css/theme.css +++ b/nikola/data/themes/bootstrap3/assets/css/theme.css @@ -51,12 +51,6 @@ div.figure > a > img { margin-right: auto; } -div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { - /* Issue 277 */ - border: 1px solid #aaa; - border-radius: 5px; -} - blockquote p, blockquote { font-size: 17.5px; font-weight: 300; diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js index f83073f..f83073f 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js index bafc4e0..bafc4e0 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js index 9b995d8..9b995d8 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js index a749232..a749232 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js index e4a595c..e4a595c 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js index 1e9a1d6..1e9a1d6 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js index 748f53b..748f53b 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-de.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js index 1154fb5..1154fb5 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-es.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js index 483e192..483e192 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-et.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js index a30b13c..a30b13c 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fa.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js index 2a7e8ad..2a7e8ad 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fi.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js index e359290..e359290 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-fr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js index 04fa276..04fa276 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gl.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js index d8105ab..d8105ab 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-gr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js index 72dddf5..72dddf5 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-he.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js index 34aa3c0..34aa3c0 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js index a87f03c..a87f03c 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-hu.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js index 31053b8..31053b8 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-id.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js index aad9d22..aad9d22 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-it.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js index 3ea27c2..3ea27c2 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ja.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js index 3e23b4a..3e23b4a 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-kr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js index 374b9bb..374b9bb 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lt.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js index 101b476..101b476 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-lv.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js index 8e14f15..8e14f15 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js index 2d03d48..2d03d48 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js index 9af0ba7..9af0ba7 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js index 34f8ab1..34f8ab1 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js index e20bd38..e20bd38 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js index 555f2e6..555f2e6 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js index bac4855..bac4855 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js index 65b0492..65b0492 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js index 99859fd..99859fd 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sk.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js index c4fd9d5..c4fd9d5 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js index d7f26e0..d7f26e0 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-sv.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js index 86fd98f..86fd98f 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-tr.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js index 7cd1336..7cd1336 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-uk.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js index e6c5965..e6c5965 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js index bd2254c..bd2254c 120000 --- a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +++ b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js diff --git a/nikola/data/themes/bootstrap/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js index c0d986b..c0d986b 100644 --- a/nikola/data/themes/bootstrap/assets/js/flowr.plugin.js +++ b/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js diff --git a/nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js index 5ee7a90..5ee7a90 120000 --- a/nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js +++ b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js diff --git a/nikola/data/themes/bootstrap/assets/js/jquery.js b/nikola/data/themes/bootstrap3/assets/js/jquery.js index 966173b..966173b 120000 --- a/nikola/data/themes/bootstrap/assets/js/jquery.js +++ b/nikola/data/themes/bootstrap3/assets/js/jquery.js diff --git a/nikola/data/themes/bootstrap3/parent b/nikola/data/themes/bootstrap3/parent index b7c200a..df967b9 100644 --- a/nikola/data/themes/bootstrap3/parent +++ b/nikola/data/themes/bootstrap3/parent @@ -1 +1 @@ -bootstrap +base diff --git a/nikola/data/themes/bootstrap3/templates/base.tmpl b/nikola/data/themes/bootstrap3/templates/base.tmpl index 16aebce..5f90733 100644 --- a/nikola/data/themes/bootstrap3/templates/base.tmpl +++ b/nikola/data/themes/bootstrap3/templates/base.tmpl @@ -13,10 +13,10 @@ ${template_hooks['extra_head']()} <!-- Menubar --> -<nav class="navbar navbar-inverse navbar-static-top" role="navigation"> +<nav class="navbar navbar-inverse navbar-static-top"> <div class="container"><!-- This keeps the margins nice --> <div class="navbar-header"> - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> @@ -32,7 +32,7 @@ ${template_hooks['extra_head']()} % endif </a> </div><!-- /.navbar-header --> - <div class="collapse navbar-collapse navbar-ex1-collapse"> + <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false"> <ul class="nav navbar-nav"> ${base.html_navigation_links()} ${template_hooks['menu']()} diff --git a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl index 8ea843a..a1e7508 100644 --- a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl +++ b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl @@ -26,17 +26,20 @@ dir="rtl" \ lang="${lang}"> <head> <meta charset="utf-8"> + <base href="${abs_link(permalink)}"> %if description: <meta name="description" content="${description}"> %endif <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>${title|striphtml} | ${blog_title|striphtml}</title> + %if title == blog_title: + <title>${blog_title|h}</title> + %else: + <title>${title|h} | ${blog_title|h}</title> + %endif ${html_stylesheets()} ${html_feedlinks()} - %if permalink: - <link rel="canonical" href="${abs_link(permalink)}"> - %endif + <link rel="canonical" href="${abs_link(permalink)}"> %if favicons: %for name, file, size in favicons: @@ -129,7 +132,7 @@ lang="${lang}"> <%def name="html_navigation_links()"> %for url, text in navigation_links[lang]: % if isinstance(url, tuple): - <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">${text} <b class="caret"></b></a> + <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text} <b class="caret"></b></a> <ul class="dropdown-menu"> %for suburl, text in url: % if rel_link(permalink, suburl) == "#": @@ -154,7 +157,7 @@ lang="${lang}"> ${rss_link} %elif generate_rss: %if len(translations) > 1: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}"> %endfor %else: @@ -163,7 +166,7 @@ lang="${lang}"> %endif %if generate_atom: %if len(translations) > 1: - %for language in translations: + %for language in sorted(translations): <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}"> %endfor %else: @@ -173,7 +176,7 @@ lang="${lang}"> </%def> <%def name="html_translations()"> - %for langname in translations.keys(): + %for langname in sorted(translations): %if langname != lang: <li><a href="${abs_link(_link("root", None, langname))}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li> %endif diff --git a/nikola/data/themes/bootstrap/templates/post.tmpl b/nikola/data/themes/bootstrap3/templates/post.tmpl index 8d9f88a..8d9f88a 100644 --- a/nikola/data/themes/bootstrap/templates/post.tmpl +++ b/nikola/data/themes/bootstrap3/templates/post.tmpl diff --git a/nikola/data/themes/bootstrap/templates/tags.tmpl b/nikola/data/themes/bootstrap3/templates/tags.tmpl index ead3b0a..ead3b0a 100644 --- a/nikola/data/themes/bootstrap/templates/tags.tmpl +++ b/nikola/data/themes/bootstrap3/templates/tags.tmpl diff --git a/nikola/filters.py b/nikola/filters.py index 269aae9..4304860 100644 --- a/nikola/filters.py +++ b/nikola/filters.py @@ -24,7 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Utility functions to help you run filters on files.""" +"""Utility functions to help run filters on files.""" from .utils import req_missing from functools import wraps @@ -42,9 +42,12 @@ except ImportError: def apply_to_binary_file(f): - """Take a function f that transforms a data argument, and returns + """Apply a filter to a binary file. + + 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 binary mode.""" + in place. Reads files in binary mode. + """ @wraps(f) def f_in_file(fname): with open(fname, 'rb') as inf: @@ -57,9 +60,12 @@ def apply_to_binary_file(f): def apply_to_text_file(f): - """Take a function f that transforms a data argument, and returns + """Apply a filter to a text file. + + 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.""" + in place. Reads files in UTF-8. + """ @wraps(f) def f_in_file(fname): with io.open(fname, 'r', encoding='utf-8') as inf: @@ -72,7 +78,7 @@ def apply_to_text_file(f): def list_replace(the_list, find, replacement): - "Replace 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 @@ -93,7 +99,6 @@ def runinplace(command, infile): You can also supply command as a list. """ - if not isinstance(command, list): command = shlex.split(command) @@ -118,6 +123,7 @@ def runinplace(command, infile): def yui_compressor(infile): + """Run YUI Compressor on a file.""" yuicompressor = False try: subprocess.call('yui-compressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) @@ -136,39 +142,48 @@ def yui_compressor(infile): def closure_compiler(infile): + """Run closure-compiler on a file.""" return runinplace(r'closure-compiler --warning_level QUIET --js %1 --js_output_file %2', infile) def optipng(infile): + """Run optipng on a file.""" return runinplace(r"optipng -preserve -o2 -quiet %1", infile) def jpegoptim(infile): + """Run jpegoptim on a file.""" return runinplace(r"jpegoptim -p --strip-all -q %1", infile) def html_tidy_withconfig(infile): + """Run HTML Tidy with tidy5.conf as config file.""" return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent -config tidy5.conf -modify %1") def html_tidy_nowrap(infile): + """Run HTML Tidy without line wrapping.""" return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1") def html_tidy_wrap(infile): + """Run HTML Tidy with line wrapping.""" return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1") def html_tidy_wrap_attr(infile): + """Run HTML tidy with line wrapping and attribute indentation.""" return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes yes --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1") def html_tidy_mini(infile): + """Run HTML tidy with minimal settings.""" return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --tidy-mark no --drop-empty-elements no -modify %1") def _html_tidy_runner(infile, options): - """ Warnings (returncode 1) are not critical, and *everything* is a warning """ + """Run HTML Tidy.""" + # Warnings (returncode 1) are not critical, and *everything* is a warning. try: status = runinplace(r"tidy5 " + options, infile) except subprocess.CalledProcessError as err: @@ -178,6 +193,7 @@ def _html_tidy_runner(infile, options): @apply_to_text_file def html5lib_minify(data): + """Minify with html5lib.""" import html5lib import html5lib.serializer data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'), @@ -193,6 +209,7 @@ def html5lib_minify(data): @apply_to_text_file def html5lib_xmllike(data): + """Transform document to an XML-like form with html5lib.""" import html5lib import html5lib.serializer data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'), @@ -207,11 +224,13 @@ def html5lib_xmllike(data): @apply_to_text_file def minify_lines(data): + """Do nothing -- deprecated filter.""" return data @apply_to_text_file def typogrify(data): + """Prettify text with typogrify.""" if typo is None: req_missing(['typogrify'], 'use the typogrify filter') @@ -226,6 +245,7 @@ def typogrify(data): @apply_to_text_file def typogrify_sans_widont(data): + """Prettify text with typogrify, skipping the widont filter.""" # typogrify with widont disabled because it caused broken headline # wrapping, see issue #1465 if typo is None: @@ -241,6 +261,7 @@ def typogrify_sans_widont(data): @apply_to_text_file def php_template_injection(data): + """Insert PHP code into Nikola templates.""" import re template = re.search('<\!-- __NIKOLA_PHP_TEMPLATE_INJECTION source\:(.*) checksum\:(.*)__ -->', data) if template: diff --git a/nikola/image_processing.py b/nikola/image_processing.py index 1e11a50..0ba139f 100644 --- a/nikola/image_processing.py +++ b/nikola/image_processing.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Process images.""" + from __future__ import unicode_literals import datetime import os @@ -43,6 +45,7 @@ except ImportError: class ImageProcessor(object): + """Apply image operations.""" image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff'] diff --git a/nikola/nikola.py b/nikola/nikola.py index 2a15568..e0af7ad 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The main Nikola site object.""" + from __future__ import print_function, unicode_literals import io from collections import defaultdict @@ -60,8 +62,9 @@ from .plugin_categories import ( Command, LateTask, PageCompiler, - RestExtension, + CompilerExtension, MarkdownExtension, + RestExtension, Task, TaskMultiplier, TemplateSystem, @@ -85,7 +88,7 @@ DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}' config_changed = utils.config_changed -__all__ = ['Nikola'] +__all__ = ('Nikola',) # We store legal values for some setting here. For internal use. LEGAL_VALUES = { @@ -123,6 +126,7 @@ LEGAL_VALUES = { 'ko': 'Korean', 'nb': 'Norwegian Bokmål', 'nl': 'Dutch', + 'pa': 'Punjabi', 'pl': 'Polish', 'pt_br': 'Portuguese (Brasil)', 'ru': 'Russian', @@ -135,6 +139,29 @@ LEGAL_VALUES = { 'uk': 'Ukrainian', 'zh_cn': 'Chinese (Simplified)', }, + '_WINDOWS_LOCALE_GUESSES': { + # TODO incomplete + # some languages may need that the appropiate Microsoft Language Pack be instaled. + "bg": "Bulgarian", + "ca": "Catalan", + "de": "German", + "el": "Greek", + "en": "English", + "eo": "Esperanto", + "es": "Spanish", + "fa": "Farsi", # Persian + "fr": "French", + "hr": "Croatian", + "it": "Italian", + "jp": "Japanese", + "nl": "Dutch", + "pl": "Polish", + "pt_br": "Portuguese_Brazil", + "ru": "Russian", + "sl_si": "Slovenian", + "tr_tr": "Turkish", + "zh_cn": "Chinese_China", # 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 @@ -238,7 +265,7 @@ LEGAL_VALUES = { def _enclosure(post, lang): - '''Default implementation of enclosures''' + """Add an enclosure to RSS.""" enclosure = post.meta('enclosure', lang) if enclosure: length = 0 @@ -256,7 +283,6 @@ class Nikola(object): def __init__(self, **config): """Setup proper environment for running tasks.""" - # Register our own path handlers self.path_handlers = { 'slug': self.slug_path, @@ -272,6 +298,7 @@ class Nikola(object): self.posts_per_month = defaultdict(list) self.posts_per_tag = defaultdict(list) self.posts_per_category = defaultdict(list) + self.tags_per_language = defaultdict(list) self.post_per_file = {} self.timeline = [] self.pages = [] @@ -279,7 +306,7 @@ class Nikola(object): self._template_system = None self._THEMES = None self.debug = DEBUG - self.loghandlers = [] + self.loghandlers = utils.STDERR_HANDLER # TODO remove on v8 self.colorful = config.pop('__colorful__', False) self.invariant = config.pop('__invariant__', False) self.quiet = config.pop('__quiet__', False) @@ -422,7 +449,7 @@ class Nikola(object): 'TAG_PAGES_DESCRIPTIONS': {}, 'TAGLIST_MINIMUM_POSTS': 1, 'TEMPLATE_FILTERS': {}, - 'THEME': 'bootstrap', + 'THEME': 'bootstrap3', 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky', 'THEME_REVEAL_CONFIG_TRANSITION': 'cube', 'THUMBNAIL_SIZE': 180, @@ -661,6 +688,37 @@ class Nikola(object): self.tzinfo = dateutil.tz.gettz() self.config['__tzinfo__'] = self.tzinfo + # Store raw compilers for internal use (need a copy for that) + self.config['_COMPILERS_RAW'] = {} + for k, v in self.config['COMPILERS'].items(): + self.config['_COMPILERS_RAW'][k] = list(v) + + compilers = defaultdict(set) + # Also add aliases for combinations with TRANSLATIONS_PATTERN + 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 + # Remove compilers that match nothing in POSTS/PAGES + # And put them in "bad compilers" + pp_exts = set([os.path.splitext(x[0])[1] for x in self.config['post_pages']]) + self.config['COMPILERS'] = {} + self.disabled_compilers = {} + self.bad_compilers = set([]) + for k, v in compilers.items(): + if pp_exts.intersection(v): + self.config['COMPILERS'][k] = sorted(list(v)) + else: + self.bad_compilers.add(k) + + self._set_global_context() + + def init_plugins(self, commands_only=False): + """Load plugins as needed.""" self.plugin_manager = PluginManager(categories_filter={ "Command": Command, "Task": Task, @@ -668,13 +726,14 @@ class Nikola(object): "TemplateSystem": TemplateSystem, "PageCompiler": PageCompiler, "TaskMultiplier": TaskMultiplier, - "RestExtension": RestExtension, + "CompilerExtension": CompilerExtension, "MarkdownExtension": MarkdownExtension, + "RestExtension": RestExtension, "SignalHandler": SignalHandler, "ConfigPlugin": ConfigPlugin, "PostScanner": PostScanner, }) - self.plugin_manager.setPluginInfoExtension('plugin') + self.plugin_manager.getPluginLocator().setPluginInfoExtension('plugin') extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS'] if sys.version_info[0] == 3: places = [ @@ -689,8 +748,36 @@ class Nikola(object): os.path.expanduser('~/.nikola/plugins'), ] + [utils.sys_encode(path) for path in extra_plugins_dirs if path] - self.plugin_manager.setPluginPlaces(places) - self.plugin_manager.collectPlugins() + self.plugin_manager.getPluginLocator().setPluginPlaces(places) + self.plugin_manager.locatePlugins() + bad_candidates = set([]) + for p in self.plugin_manager._candidates: + if commands_only: + if p[-1].details.has_option('Nikola', 'plugincategory'): + # FIXME TemplateSystem should not be needed + if p[-1].details.get('Nikola', 'PluginCategory') not in {'Command', 'Template'}: + bad_candidates.add(p) + else: # Not commands-only + # Remove compilers we don't use + if p[-1].name in self.bad_compilers: + bad_candidates.add(p) + self.disabled_compilers[p[-1].name] = p + utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name) + if p[-1].name not in self.config['COMPILERS'] and \ + p[-1].details.has_option('Nikola', 'plugincategory') and p[-1].details.get('Nikola', 'PluginCategory') == 'Compiler': + bad_candidates.add(p) + self.disabled_compilers[p[-1].name] = p + utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name) + # Remove blacklisted plugins + if p[-1].name in self.config['DISABLED_PLUGINS']: + bad_candidates.add(p) + utils.LOGGER.debug('Not loading disabled plugin {}', p[-1].name) + # Remove compiler extensions we don't need + if p[-1].details.has_option('Nikola', 'compiler') and p[-1].details.get('Nikola', 'compiler') in self.disabled_compilers: + bad_candidates.add(p) + utils.LOGGER.debug('Not loading comopiler extension {}', p[-1].name) + self.plugin_manager._candidates = list(set(self.plugin_manager._candidates) - bad_candidates) + self.plugin_manager.loadPlugins() self._activate_plugins_of_category("SignalHandler") @@ -709,30 +796,28 @@ class Nikola(object): self._activate_plugins_of_category("LateTask") self._activate_plugins_of_category("TaskMultiplier") - # Store raw compilers for internal use (need a copy for that) - self.config['_COMPILERS_RAW'] = {} - for k, v in self.config['COMPILERS'].items(): - self.config['_COMPILERS_RAW'][k] = list(v) - - compilers = defaultdict(set) - # Also add aliases for combinations with TRANSLATIONS_PATTERN - 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 + self.compiler_extensions = self._activate_plugins_of_category("CompilerExtension") for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"): if plugin_info.name in self.config["COMPILERS"].keys(): self.plugin_manager.activatePluginByName(plugin_info.name) plugin_info.plugin_object.set_site(self) + # Load compiler plugins + self.compilers = {} + self.inverse_compilers = {} + + for plugin_info in self.plugin_manager.getPluginsOfCategory( + "PageCompiler"): + self.compilers[plugin_info.name] = \ + plugin_info.plugin_object + + self._activate_plugins_of_category("ConfigPlugin") + + signal('configured').send(self) + + def _set_global_context(self): + """Create global context from configuration.""" self._GLOBAL_CONTEXT['url_type'] = self.config['URL_TYPE'] self._GLOBAL_CONTEXT['timezone'] = self.tzinfo self._GLOBAL_CONTEXT['_link'] = self.link @@ -801,41 +886,21 @@ class Nikola(object): self._GLOBAL_CONTEXT['hidden_categories'] = self.config.get('HIDDEN_CATEGORIES') self._GLOBAL_CONTEXT['url_replacer'] = self.url_replacer - # IPython theme configuration. If a website can potentially have ipynb - # posts (as determined by checking POSTS/PAGES against ipynb - # extensions), we should enable the IPython CSS (leaving that up to the - # theme itself). + # IPython theme configuration. If a website has ipynb enabled in post_pages + # we should enable the IPython CSS (leaving that up to the theme itself). - self._GLOBAL_CONTEXT['needs_ipython_css'] = False - for i in self.config['post_pages']: - if os.path.splitext(i[0])[1] in self.config['COMPILERS'].get('ipynb', []): - self._GLOBAL_CONTEXT['needs_ipython_css'] = True + self._GLOBAL_CONTEXT['needs_ipython_css'] = 'ipynb' in self.config['COMPILERS'] self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {})) - # Load compiler plugins - self.compilers = {} - self.inverse_compilers = {} - - for plugin_info in self.plugin_manager.getPluginsOfCategory( - "PageCompiler"): - self.compilers[plugin_info.name] = \ - plugin_info.plugin_object - - self._activate_plugins_of_category("ConfigPlugin") - - signal('configured').send(self) - def _activate_plugins_of_category(self, category): """Activate all the plugins of a given category and return them.""" + # this code duplicated in tests/base.py plugins = [] for plugin_info in self.plugin_manager.getPluginsOfCategory(category): - if plugin_info.name in self.config.get('DISABLED_PLUGINS'): - self.plugin_manager.removePluginFromCategory(plugin_info, category) - else: - self.plugin_manager.activatePluginByName(plugin_info.name) - plugin_info.plugin_object.set_site(self) - plugins.append(plugin_info) + self.plugin_manager.activatePluginByName(plugin_info.name) + plugin_info.plugin_object.set_site(self) + plugins.append(plugin_info) return plugins def _get_themes(self): @@ -843,8 +908,8 @@ class Nikola(object): try: self._THEMES = utils.get_theme_chain(self.config['THEME']) except Exception: - utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME'])) - self.config['THEME'] = 'bootstrap' + utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap3' instead.'''.format(self.config['THEME'])) + self.config['THEME'] = 'bootstrap3' return self._get_themes() # Check consistency of USE_CDN and the current THEME (Issue #386) if self.config['USE_CDN'] and self.config['USE_CDN_WARNING']: @@ -909,11 +974,11 @@ class Nikola(object): template_system = property(_get_template_system) def get_compiler(self, source_name): - """Get the correct compiler for a post from `conf.COMPILERS` + """Get the correct compiler for a post from `conf.COMPILERS`. + To make things easier for users, the mapping in conf.py is - compiler->[extensions], although this is less convenient for us. The - majority of this function is reversing that dictionary and error - checking. + compiler->[extensions], although this is less convenient for us. + The majority of this function is reversing that dictionary and error checking. """ ext = os.path.splitext(source_name)[1] try: @@ -925,9 +990,9 @@ class Nikola(object): len([ext_ for ext_ in exts if source_name.endswith(ext_)]) > 0] if len(langs) != 1: if len(set(langs)) > 1: - exit("Your file extension->compiler definition is" - "ambiguous.\nPlease remove one of the file extensions" - "from 'COMPILERS' in conf.py\n(The error is in" + exit("Your file extension->compiler definition is " + "ambiguous.\nPlease remove one of the file extensions " + "from 'COMPILERS' in conf.py\n(The error is in " "one of {0})".format(', '.join(langs))) elif len(langs) > 1: langs = langs[:1] @@ -988,13 +1053,26 @@ class Nikola(object): utils.makedirs(os.path.dirname(output_name)) parser = lxml.html.HTMLParser(remove_blank_text=True) doc = lxml.html.document_fromstring(data, parser) - doc.rewrite_links(lambda dst: self.url_replacer(src, dst, context['lang'])) + self.rewrite_links(doc, src, context['lang']) 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) + def rewrite_links(self, doc, src, lang): + """Replace links in document to point to the right places.""" + # First let lxml replace most of them + doc.rewrite_links(lambda dst: self.url_replacer(src, dst, lang), resolve_base_href=False) + + # lxml ignores srcset in img and source elements, so do that by hand + objs = list(doc.findall('*//img')) + list(doc.findall('*//source')) + for obj in objs: + if 'srcset' in obj.attrib: + urls = [u.strip() for u in obj.attrib['srcset'].split(',')] + urls = [self.url_replacer(src, dst, lang) for dst in urls] + obj.set('srcset', ', '.join(urls)) + def url_replacer(self, src, dst, lang=None, url_type=None): - """URL mangler. + """Mangle URLs. * Replaces link:// URLs with real links * Makes dst relative to src @@ -1111,14 +1189,13 @@ class Nikola(object): def generic_rss_renderer(self, lang, title, link, description, timeline, output_path, rss_teasers, rss_plain, feed_length=10, feed_url=None, enclosure=_enclosure, rss_links_append_query=None): - - """Takes all necessary data, and renders a RSS feed in output_path.""" + """Take all necessary data, and render a RSS feed in output_path.""" rss_obj = utils.ExtendedRSS2( title=title, link=link, description=description, lastBuildDate=datetime.datetime.utcnow(), - generator='http://getnikola.com/', + generator='https://getnikola.com/', language=lang ) @@ -1193,7 +1270,7 @@ class Nikola(object): rss_file.write(data) def path(self, kind, name, lang=None, is_link=False): - """Build the path to a certain kind of page. + r"""Build the path to a certain kind of page. These are mostly defined by plugins by registering via the register_path_handler method, except for slug, post_path, root @@ -1223,9 +1300,8 @@ class Nikola(object): (ex: "/archive/index.html"). If is_link is False, the path is relative to output and uses the platform's separator. - (ex: "archive\\index.html") + (ex: "archive\index.html") """ - if lang is None: lang = utils.LocaleBorg().current_lang @@ -1248,13 +1324,13 @@ class Nikola(object): return "" def post_path(self, name, lang): - """post_path path handler""" + """Handle post_path paths.""" return [_f for _f in [self.config['TRANSLATIONS'][lang], os.path.dirname(name), self.config['INDEX_FILE']] if _f] def root_path(self, name, lang): - """root_path path handler""" + """Handle root_path paths.""" d = self.config['TRANSLATIONS'][lang] if d: return [d, ''] @@ -1262,7 +1338,7 @@ class Nikola(object): return [] def slug_path(self, name, lang): - """slug path handler""" + """Handle slug paths.""" results = [p for p in self.timeline if p.meta('slug') == name] if not results: utils.LOGGER.warning("Cannot resolve path request for slug: {0}".format(name)) @@ -1272,7 +1348,7 @@ class Nikola(object): return [_f for _f in results[0].permalink(lang).split('/') if _f] def filename_path(self, name, lang): - """filename path handler""" + """Handle filename paths.""" results = [p for p in self.timeline if p.source_path == name] if not results: utils.LOGGER.warning("Cannot resolve path request for filename: {0}".format(name)) @@ -1282,15 +1358,18 @@ class Nikola(object): return [_f for _f in results[0].permalink(lang).split('/') if _f] def register_path_handler(self, kind, f): + """Register a path handler.""" if kind in self.path_handlers: utils.LOGGER.warning('Conflicting path handlers for kind: {0}'.format(kind)) else: self.path_handlers[kind] = f def link(self, *args): + """Create a link.""" return self.path(*args, is_link=True) def abs_link(self, dst, protocol_relative=False): + """Get an absolute link.""" # Normalize if dst: # Mako templates and empty strings evaluate to False dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) @@ -1302,6 +1381,7 @@ class Nikola(object): return url def rel_link(self, src, dst): + """Get a relative link.""" # Normalize src = urljoin(self.config['BASE_URL'], src) dst = urljoin(src, dst) @@ -1326,8 +1406,7 @@ class Nikola(object): return '/'.join(['..'] * (len(src_elems) - i - 1) + dst_elems[i:]) def file_exists(self, path, not_empty=False): - """Returns True if the file exists. If not_empty is True, - it also has to be not empty.""" + """Check if the file exists. If not_empty is True, it also must not be empty.""" exists = os.path.exists(path) if exists and not_empty: exists = os.stat(path).st_size > 0 @@ -1341,8 +1420,9 @@ class Nikola(object): return task def gen_tasks(self, name, plugin_category, doc=''): - + """Generate tasks.""" def flatten(task): + """Flatten lists of tasks.""" if isinstance(task, dict): yield task else: @@ -1377,6 +1457,7 @@ class Nikola(object): } def parse_category_name(self, category_name): + """Parse a category name into a hierarchy.""" if self.config['CATEGORY_ALLOW_HIERARCHIES']: try: return utils.parse_escaped_hierarchical_category_name(category_name) @@ -1387,12 +1468,14 @@ class Nikola(object): return [category_name] if len(category_name) > 0 else [] def category_path_to_category_name(self, category_path): + """Translate a category path to a category name.""" if self.config['CATEGORY_ALLOW_HIERARCHIES']: return utils.join_hierarchical_category_path(category_path) else: return ''.join(category_path) def _add_post_to_category(self, post, category_name): + """Add a post to a category.""" category_path = self.parse_category_name(category_name) current_path = [] current_subtree = self.category_hierarchy @@ -1404,10 +1487,12 @@ class Nikola(object): self.posts_per_category[self.category_path_to_category_name(current_path)].append(post) def _sort_category_hierarchy(self): + """Sort category hierarchy.""" # First create a hierarchy of TreeNodes self.category_hierarchy_lookup = {} def create_hierarchy(cat_hierarchy, parent=None): + """Create category hierarchy.""" result = [] for name, children in cat_hierarchy.items(): node = utils.TreeNode(name, parent) @@ -1426,7 +1511,7 @@ class Nikola(object): def scan_posts(self, really=False, ignore_quit=False, quiet=False): """Scan all the posts. - Ignoring quiet. + The `quiet` option is ignored. """ if self._scanned and not really: return @@ -1438,6 +1523,7 @@ class Nikola(object): self.posts_per_month = defaultdict(list) self.posts_per_tag = defaultdict(list) self.posts_per_category = defaultdict(list) + self.tags_per_language = defaultdict(list) self.category_hierarchy = {} self.post_per_file = {} self.timeline = [] @@ -1470,6 +1556,8 @@ class Nikola(object): else: slugged_tags.add(utils.slugify(tag, force=True)) self.posts_per_tag[tag].append(post) + for lang in self.config['TRANSLATIONS'].keys(): + self.tags_per_language[lang].extend(post.tags_for_language(lang)) self._add_post_to_category(post, post.meta('category')) if post.is_post: @@ -1495,6 +1583,8 @@ class Nikola(object): quit = True self.post_per_file[dest] = post self.post_per_file[src_dest] = post + # deduplicate tags_per_language + self.tags_per_language[lang] = list(set(self.tags_per_language[lang])) # Sort everything. @@ -1514,9 +1604,9 @@ class Nikola(object): sys.exit(1) signal('scanned').send(self) - def generic_page_renderer(self, lang, post, filters): + def generic_page_renderer(self, lang, post, filters, context=None): """Render post fragments to final HTML pages.""" - context = {} + context = context.copy() if context else {} deps = post.deps(lang) + \ self.template_system.template_deps(post.template_name) deps.extend(utils.get_asset_path(x, self.THEMES) for x in ('bundles', 'parent', 'engine')) @@ -1526,6 +1616,8 @@ class Nikola(object): context['title'] = post.title(lang) context['description'] = post.description(lang) context['permalink'] = post.permalink(lang) + if 'pagekind' not in context: + context['pagekind'] = ['generic_page'] if post.use_in_feeds: context['enable_comments'] = True else: @@ -1557,7 +1649,7 @@ class Nikola(object): task = { 'name': os.path.normpath(output_name), - 'file_dep': deps, + 'file_dep': sorted(deps), 'targets': [output_name], 'actions': [(self.render_template, [post.template_name, output_name, context])], @@ -1569,9 +1661,9 @@ class Nikola(object): def generic_post_list_renderer(self, lang, posts, output_name, template_name, filters, extra_context): - """Renders pages with lists of posts.""" - - deps = self.template_system.template_deps(template_name) + """Render pages with lists of posts.""" + deps = [] + deps += self.template_system.template_deps(template_name) uptodate_deps = [] for post in posts: deps += post.deps(lang) @@ -1600,7 +1692,7 @@ class Nikola(object): task = { 'name': os.path.normpath(output_name), 'targets': [output_name], - 'file_dep': deps, + 'file_dep': sorted(deps), 'actions': [(self.render_template, [template_name, output_name, context])], 'clean': True, @@ -1611,9 +1703,10 @@ class Nikola(object): def atom_feed_renderer(self, lang, posts, output_path, filters, extra_context): - """Renders Atom feeds and archives with lists of posts. Feeds are - considered archives when no future updates to them are expected""" + """Render Atom feeds and archives with lists of posts. + Feeds are considered archives when no future updates to them are expected. + """ def atom_link(link_rel, link_type, link_href): link = lxml.etree.Element("link") link.set("rel", link_rel) @@ -1662,7 +1755,7 @@ class Nikola(object): feed_id = lxml.etree.SubElement(feed_root, "id") feed_id.text = self.abs_link(context["feedlink"]) feed_updated = lxml.etree.SubElement(feed_root, "updated") - feed_updated.text = datetime.datetime.now(tz=dateutil.tz.tzutc()).replace(microsecond=0).isoformat() + feed_updated.text = post.formatted_date('webiso', datetime.datetime.now(tz=dateutil.tz.tzutc())) feed_author = lxml.etree.SubElement(feed_root, "author") feed_author_name = lxml.etree.SubElement(feed_author, "name") feed_author_name.text = self.config["BLOG_AUTHOR"](lang) @@ -1726,9 +1819,9 @@ class Nikola(object): entry_id = lxml.etree.SubElement(entry_root, "id") entry_id.text = post.permalink(lang, absolute=True) entry_updated = lxml.etree.SubElement(entry_root, "updated") - entry_updated.text = post.updated.isoformat() + entry_updated.text = post.formatted_updated('webiso') entry_published = lxml.etree.SubElement(entry_root, "published") - entry_published.text = post.date.isoformat() + entry_published.text = post.formatted_date('webiso') entry_author = lxml.etree.SubElement(entry_root, "author") entry_author_name = lxml.etree.SubElement(entry_author, "name") entry_author_name.text = post.author(lang) @@ -1757,7 +1850,7 @@ class Nikola(object): atom_file.write(data) def generic_index_renderer(self, lang, posts, indexes_title, template_name, context_source, kw, basename, page_link, page_path, additional_dependencies=[]): - """Creates an index page. + """Create an index page. lang: The language posts: A list of posts @@ -1812,6 +1905,8 @@ class Nikola(object): num_pages = len(lists) for i, post_list in enumerate(lists): context = context_source.copy() + if 'pagekind' not in context: + context['pagekind'] = ['index'] ipages_i = utils.get_displayed_page_number(i, num_pages, self) if kw["indexes_pages"]: indexes_pages = kw["indexes_pages"] % ipages_i @@ -1885,8 +1980,8 @@ class Nikola(object): context["feedpagecount"] = num_pages atom_task = { "basename": basename, - "file_dep": [output_name], "name": atom_output_name, + "file_dep": sorted([_.base_path for _ in post_list]), "targets": [atom_output_name], "actions": [(self.atom_feed_renderer, (lang, @@ -1913,11 +2008,12 @@ class Nikola(object): }, kw["filters"]) def __repr__(self): + """Representation of a Nikola site.""" 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 + """Sanitize all locales availble in Nikola. There will be one locale for each language in translations. @@ -1994,10 +2090,7 @@ def sanitized_locales(locale_fallback, locale_default, locales, translations): def is_valid_locale(locale_n): - """True if locale_n is acceptable for locale.setlocale - - for py2x compat locale_n should be of type str - """ + """Check if locale (type str) is valid.""" try: locale.setlocale(locale.LC_ALL, locale_n) return True @@ -2006,7 +2099,7 @@ def is_valid_locale(locale_n): def valid_locale_fallback(desired_locale=None): - """returns a default fallback_locale, a string that locale.setlocale will accept + """Provide a default fallback_locale, a string that locale.setlocale will accept. If desired_locale is provided must be of type str for py2x compatibility """ @@ -2032,13 +2125,15 @@ def valid_locale_fallback(desired_locale=None): def guess_locale_from_lang_windows(lang): - locale_n = str(_windows_locale_guesses.get(lang, None)) + """Guess a locale, basing on Windows language.""" + locale_n = str(LEGAL_VALUES['_WINDOWS_LOCALE_GUESSES'].get(lang, None)) if not is_valid_locale(locale_n): locale_n = None return locale_n def guess_locale_from_lang_posix(lang): + """Guess a locale, basing on POSIX system language.""" # compatibility v6.0.4 if is_valid_locale(str(lang)): locale_n = str(lang) @@ -2065,28 +2160,3 @@ def workaround_empty_LC_ALL_posix(): os.environ['LC_ALL'] = lc_time except Exception: pass - - -_windows_locale_guesses = { - # some languages may need that the appropiate Microsoft's Language Pack - # be instaled; the 'str' bit will be added in the guess function - "bg": "Bulgarian", - "ca": "Catalan", - "de": "German", - "el": "Greek", - "en": "English", - "eo": "Esperanto", - "es": "Spanish", - "fa": "Farsi", # Persian - "fr": "French", - "hr": "Croatian", - "it": "Italian", - "jp": "Japanese", - "nl": "Dutch", - "pl": "Polish", - "pt_br": "Portuguese_Brazil", - "ru": "Russian", - "sl_si": "Slovenian", - "tr_tr": "Turkish", - "zh_cn": "Chinese_China", # Chinese (Simplified) -} diff --git a/nikola/packages/__init__.py b/nikola/packages/__init__.py index 0a704f5..34beaea 100644 --- a/nikola/packages/__init__.py +++ b/nikola/packages/__init__.py @@ -1 +1 @@ -# Packages vendored in by Nikola. +"""Third-party packages for Nikola.""" diff --git a/nikola/packages/tzlocal/__init__.py b/nikola/packages/tzlocal/__init__.py index 10716ec..4a6b1d6 100644 --- a/nikola/packages/tzlocal/__init__.py +++ b/nikola/packages/tzlocal/__init__.py @@ -1,3 +1,5 @@ +"""tzlocal init.""" + import sys if sys.platform == 'win32': from .win32 import get_localzone, reload_localzone # NOQA diff --git a/nikola/packages/tzlocal/darwin.py b/nikola/packages/tzlocal/darwin.py index 8bfe807..0dbf1c1 100644 --- a/nikola/packages/tzlocal/darwin.py +++ b/nikola/packages/tzlocal/darwin.py @@ -1,4 +1,5 @@ -from __future__ import with_statement +"""tzlocal for OS X.""" + import os import dateutil.tz import subprocess diff --git a/nikola/packages/tzlocal/unix.py b/nikola/packages/tzlocal/unix.py index 4cd3752..8f7fc84 100644 --- a/nikola/packages/tzlocal/unix.py +++ b/nikola/packages/tzlocal/unix.py @@ -1,3 +1,5 @@ +"""tzlocal for UNIX.""" + from __future__ import with_statement import os import re @@ -7,7 +9,7 @@ _cache_tz = None def _get_localzone(): - """Tries to find the local timezone configuration. + """Try 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 @@ -15,8 +17,8 @@ def _get_localzone(): 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.""" - + In normal usage you call the function without parameters. + """ tz = os.environ.get('TZ') if tz and tz[0] == ':': tz = tz[1:] diff --git a/nikola/packages/tzlocal/win32.py b/nikola/packages/tzlocal/win32.py index 7005422..cb19284 100644 --- a/nikola/packages/tzlocal/win32.py +++ b/nikola/packages/tzlocal/win32.py @@ -1,3 +1,5 @@ +"""tzlocal for Windows.""" + try: import _winreg as winreg except ImportError: @@ -22,6 +24,7 @@ def valuestodict(key): def get_localzone_name(): + """Get local time zone 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 @@ -78,7 +81,7 @@ def get_localzone_name(): def get_localzone(): - """Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone.""" + """Return the zoneinfo-based tzinfo object that matches the Windows-configured timezone.""" global _cache_tz if _cache_tz is None: _cache_tz = get_localzone_name() diff --git a/nikola/packages/tzlocal/windows_tz.py b/nikola/packages/tzlocal/windows_tz.py index 1084478..c171aa5 100644 --- a/nikola/packages/tzlocal/windows_tz.py +++ b/nikola/packages/tzlocal/windows_tz.py @@ -1,3 +1,4 @@ +"""Windows timezone names.""" # This file is autogenerated by the get_windows_info.py script # Do not edit. win_tz = { diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index e9af6b5..ecbbb82 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Nikola plugin categories.""" + from __future__ import absolute_import import sys import os @@ -34,7 +36,7 @@ from doit.cmd_base import Command as DoitCommand from .utils import LOGGER, first_line -__all__ = [ +__all__ = ( 'Command', 'LateTask', 'PageCompiler', @@ -46,22 +48,20 @@ __all__ = [ 'SignalHandler', 'ConfigPlugin', 'PostScanner', -] +) class BasePlugin(IPlugin): + """Base plugin class.""" def set_site(self, site): - """Sets site, which is a Nikola instance.""" + """Set 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.""" - + """Inject 'templates/<engine>' (if exists) very early in the theme chain.""" try: # Sorry, found no other way to get this mod_path = sys.modules[self.__class__.__module__].__file__ @@ -79,21 +79,22 @@ class BasePlugin(IPlugin): pass def inject_dependency(self, target, dependency): - """Add 'dependency' to the target task's task_deps""" + """Add 'dependency' to the target task's task_deps.""" self.site.injected_deps[target].append(dependency) class PostScanner(BasePlugin): + """The scan method of these plugins is called by Nikola.scan_posts.""" def scan(self): - """Creates a list of posts from some source. Returns a list of Post objects.""" + """Create a list of posts from some source. Returns a list of Post objects.""" raise NotImplementedError() class Command(BasePlugin, DoitCommand): - """These plugins are exposed via the command line. - They implement the doit Command interface.""" + + """Doit command implementation.""" name = "dummy_command" @@ -105,17 +106,18 @@ class Command(BasePlugin, DoitCommand): needs_config = True def __init__(self, *args, **kwargs): + """Initialize a command.""" BasePlugin.__init__(self, *args, **kwargs) DoitCommand.__init__(self) def __call__(self, config=None, **kwargs): + """Reset doit arguments (workaround).""" self._doitargs = kwargs DoitCommand.__init__(self, config, **kwargs) return self def execute(self, options=None, args=None): - """Check if the command can run in the current environment, - fail if needed, or call _execute.""" + """Check if the command can run in the current environment, fail if needed, or call _execute.""" options = options or {} args = args or [] @@ -126,6 +128,7 @@ class Command(BasePlugin, DoitCommand): def _execute(self, options, args): """Do whatever this command does. + @param options (dict) with values from cmd_options @param args (list) list of positional arguments """ @@ -133,7 +136,7 @@ class Command(BasePlugin, DoitCommand): def help(self): - """return help text""" + """Return help text for a command.""" text = [] text.append("Purpose: %s" % self.doc_purpose) text.append("Usage: nikola %s %s" % (self.name, self.doc_usage)) @@ -153,7 +156,8 @@ DoitCommand.help = help class BaseTask(BasePlugin): - """Plugins of this type are task generators.""" + + """Base for task generators.""" name = "dummy_task" @@ -162,11 +166,11 @@ class BaseTask(BasePlugin): is_default = True def gen_tasks(self): - """Task generator.""" + """Generate tasks.""" raise NotImplementedError() def group_task(self): - """dict for group task""" + """Return dict for group task.""" return { 'basename': self.name, 'name': None, @@ -175,32 +179,35 @@ class BaseTask(BasePlugin): class Task(BaseTask): - """Plugins of this type are task generators.""" + + """Task generator.""" name = "dummy_task" class LateTask(BaseTask): - """Plugins of this type are executed after all plugins of type Task.""" + + """Late task generator (plugin executed after all Task plugins).""" name = "dummy_latetask" class TemplateSystem(BasePlugin): - """Plugins of this type wrap templating systems.""" + + """Provide support for templating systems.""" name = "dummy_templates" def set_directories(self, directories, cache_folder): - """Sets the list of folders where templates are located and cache.""" + """Set the list of folders where templates are located and cache.""" raise NotImplementedError() def template_deps(self, template_name): - """Returns filenames which are dependencies for a template.""" + """Return filenames which are dependencies for a template.""" raise NotImplementedError() def render_template(self, template_name, output_name, context): - """Renders template to a file using context. + """Render template to a file using context. This must save the data to output_name *and* return it so that the caller may do additional processing. @@ -208,28 +215,28 @@ class TemplateSystem(BasePlugin): raise NotImplementedError() def render_template_to_string(self, template, context): - """Renders template to a string using context. """ + """Render 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.""" + """Inject the directory with the lowest priority in the template search mechanism.""" raise NotImplementedError() class TaskMultiplier(BasePlugin): - """Plugins that take a task and return *more* tasks.""" + + """Take a task and return *more* tasks.""" name = "dummy multiplier" def process(self, task): - """Examine task and create more tasks. - Returns extra tasks only.""" + """Examine task and create more tasks. Returns extra tasks only.""" return [] class PageCompiler(BasePlugin): - """Plugins that compile text files into HTML.""" + + """Compile text files into HTML.""" name = "dummy_compiler" friendly_name = '' @@ -251,7 +258,8 @@ class PageCompiler(BasePlugin): """Add additional dependencies to the post object. Current main use is the ReST page compiler, which puts extra - dependencies into a .deb file.""" + dependencies into a .dep file. + """ pass def compile_html(self, source, dest, is_two_file=False): @@ -267,41 +275,81 @@ class PageCompiler(BasePlugin): return ".html" def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): - """ - Read the metadata from a post, and return a metadata dict - """ + """Read the metadata from a post, and return a metadata dict.""" return {} def split_metadata(self, data): """Split data from metadata in the raw post content. This splits in the first empty line that is NOT at the beginning - of the document.""" + of the document. + """ split_result = re.split('(\n\n|\r\n\r\n)', data.lstrip(), maxsplit=1) if len(split_result) == 1: return '', split_result[0] # ['metadata', '\n\n', 'post content'] return split_result[0], split_result[-1] + def get_compiler_extensions(self): + """Activate all the compiler extension plugins for a given compiler and return them.""" + plugins = [] + for plugin_info in self.site.compiler_extensions: + if plugin_info.plugin_object.compiler_name == self.name: + plugins.append(plugin_info) + return plugins + + +class CompilerExtension(BasePlugin): + + """An extension for a Nikola compiler. + + If you intend to implement those in your own compiler, you can: + (a) create a new plugin class for them; or + (b) use this class and filter them yourself. + If you choose (b), you should the compiler name to the .plugin + file in the Nikola/Compiler section and filter all plugins of + this category, getting the compiler name with: + p.details.get('Nikola', 'Compiler') + Note that not all compiler plugins have this option and you might + need to catch configparser.NoOptionError exceptions. + """ + + name = "dummy_compiler_extension" + compiler_name = "dummy_compiler" + + +class RestExtension(CompilerExtension): + + """Extensions for reStructuredText.""" -class RestExtension(BasePlugin): name = "dummy_rest_extension" + compiler_name = "rest" + +class MarkdownExtension(CompilerExtension): + + """Extensions for Markdown.""" -class MarkdownExtension(BasePlugin): name = "dummy_markdown_extension" + compiler_name = "markdown" class SignalHandler(BasePlugin): + + """Signal handlers.""" + name = "dummy_signal_handler" class ConfigPlugin(BasePlugin): + """A plugin that can edit config (or modify the site) on-the-fly.""" + name = "dummy_config_plugin" class Importer(Command): + """Basic structure for importing data into Nikola. The flow is: @@ -345,7 +393,7 @@ class Importer(Command): raise NotImplementedError() def read_data(self, source): - """Fetch data into self.data""" + """Fetch data into self.data.""" raise NotImplementedError() def preprocess_data(self): @@ -353,7 +401,7 @@ class Importer(Command): pass def parse_data(self): - """Convert self.data into self.items""" + """Convert self.data into self.items.""" raise NotImplementedError() def filter_data(self): diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py index 139759b..b83f43f 100644 --- a/nikola/plugins/__init__.py +++ b/nikola/plugins/__init__.py @@ -1,2 +1,5 @@ # -*- coding: utf-8 -*- + +"""Plugins for Nikola.""" + from __future__ import absolute_import diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py index f8a3a3c..073a539 100644 --- a/nikola/plugins/basic_import.py +++ b/nikola/plugins/basic_import.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Mixin for importer plugins.""" + from __future__ import unicode_literals, print_function import io import csv @@ -46,6 +48,7 @@ links = {} class ImportMixin(object): + """Mixin with common used methods.""" name = "import_mixin" @@ -68,12 +71,14 @@ class ImportMixin(object): @classmethod def get_channel_from_file(cls, filename): + """Get channel from XML file.""" tree = etree.fromstring(cls.read_xml_file(filename)) channel = tree.find('channel') return channel @staticmethod def configure_redirections(url_map): + """Configure redirections from an url_map.""" redirections = [] for k, v in url_map.items(): if not k[-1] == '/': @@ -90,6 +95,7 @@ class ImportMixin(object): return redirections def generate_base_site(self): + """Generate a base Nikola site.""" if not os.path.exists(self.output_folder): os.system('nikola init -q ' + self.output_folder) else: @@ -108,14 +114,17 @@ class ImportMixin(object): @staticmethod def populate_context(channel): + """Populate context with settings.""" raise NotImplementedError("Must be implemented by a subclass.") @classmethod def transform_content(cls, content): + """Transform content to a Nikola-friendly format.""" return content @classmethod def write_content(cls, filename, content, rewrite_html=True): + """Write content to file.""" if rewrite_html: doc = html.document_fromstring(content) doc.rewrite_links(replacer) @@ -129,6 +138,7 @@ class ImportMixin(object): @staticmethod def write_metadata(filename, title, slug, post_date, description, tags, **kwargs): + """Write metadata to meta file.""" if not description: description = "" @@ -140,6 +150,7 @@ class ImportMixin(object): @staticmethod def write_urlmap_csv(output_file, url_map): + """Write urlmap to csv file.""" utils.makedirs(os.path.dirname(output_file)) fmode = 'wb+' if sys.version_info[0] == 2 else 'w+' with io.open(output_file, fmode) as fd: @@ -148,6 +159,7 @@ class ImportMixin(object): csv_writer.writerow(item) def get_configuration_output_path(self): + """Get path for the output configuration file.""" if not self.import_into_existing_site: filename = 'conf.py' else: @@ -161,10 +173,12 @@ class ImportMixin(object): @staticmethod def write_configuration(filename, rendered_template): + """Write the configuration file.""" utils.makedirs(os.path.dirname(filename)) with io.open(filename, 'w+', encoding='utf8') as fd: fd.write(rendered_template) def replacer(dst): + """Replace links.""" return links.get(dst, dst) diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py index a1d17a6..2aa5267 100644 --- a/nikola/plugins/command/__init__.py +++ b/nikola/plugins/command/__init__.py @@ -23,3 +23,5 @@ # 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. + +"""Commands for Nikola.""" diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin index a1c6820..3e2b17d 100644 --- a/nikola/plugins/command/auto.plugin +++ b/nikola/plugins/command/auto.plugin @@ -1,9 +1,13 @@ [Core] -Name = auto -Module = auto +name = auto +module = auto [Documentation] -Author = Roberto Alsina -Version = 2.1.0 -Website = http://getnikola.com -Description = Automatically detect site changes, rebuild and optionally refresh a browser. +author = Roberto Alsina +version = 2.1.0 +website = http://getnikola.com +description = Automatically detect site changes, rebuild and optionally refresh a browser. + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/auto/__init__.py b/nikola/plugins/command/auto/__init__.py index c25ef8a..71f9624 100644 --- a/nikola/plugins/command/auto/__init__.py +++ b/nikola/plugins/command/auto/__init__.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Automatic rebuilds for Nikola.""" + from __future__ import print_function import json @@ -31,10 +33,13 @@ import mimetypes import os import re import subprocess +import sys +import time try: from urlparse import urlparse + from urllib2 import unquote except ImportError: - from urllib.parse import urlparse # NOQA + from urllib.parse import urlparse, unquote # NOQA import webbrowser from wsgiref.simple_server import make_server import wsgiref.util @@ -42,7 +47,7 @@ import wsgiref.util from blinker import signal try: from ws4py.websocket import WebSocket - from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler + from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler, WebSocketWSGIHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.messaging import TextMessage except ImportError: @@ -58,7 +63,7 @@ except ImportError: from nikola.plugin_categories import Command -from nikola.utils import req_missing, get_logger, get_theme_path +from nikola.utils import req_missing, get_logger, get_theme_path, STDERR_HANDLER LRJS_PATH = os.path.join(os.path.dirname(__file__), 'livereload.js') error_signal = signal('error') refresh_signal = signal('refresh') @@ -74,9 +79,12 @@ ERROR {} class CommandAuto(Command): - """Start debugging console.""" + + """Automatic rebuilds for Nikola.""" + name = "auto" logger = None + has_server = True doc_purpose = "builds and serves a site; automatically detects site changes, rebuilds, and optionally refreshes a browser" cmd_options = [ { @@ -100,7 +108,7 @@ class CommandAuto(Command): 'short': 'b', 'long': 'browser', 'type': bool, - 'help': 'Start a web browser.', + 'help': 'Start a web browser', 'default': False, }, { @@ -111,12 +119,18 @@ class CommandAuto(Command): 'type': bool, 'help': 'Use IPv6', }, + { + 'name': 'no-server', + 'long': 'no-server', + 'default': False, + 'type': bool, + 'help': 'Disable the server, automate rebuilds only' + }, ] def _execute(self, options, args): """Start the watcher.""" - - self.logger = get_logger('auto', self.site.loghandlers) + self.logger = get_logger('auto', STDERR_HANDLER) LRSocket.logger = self.logger if WebSocket is object and watchdog is None: @@ -166,10 +180,14 @@ class CommandAuto(Command): host = options['address'].strip('[').strip(']') or dhost + # Server can be disabled (Issue #1883) + self.has_server = not options['no-server'] + # Instantiate global observer observer = Observer() - # Watch output folders and trigger reloads - observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) + if self.has_server: + # Watch output folders and trigger reloads + observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: @@ -181,101 +199,155 @@ class CommandAuto(Command): _conf_dn = os.path.dirname(_conf_fn) observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) - observer.start() + try: + self.logger.info("Watching files for changes...") + observer.start() + except KeyboardInterrupt: + pass parent = self class Mixed(WebSocketWSGIApplication): - """A class that supports WS and HTTP protocols in the same port.""" + + """A class that supports WS and HTTP protocols on the same port.""" + def __call__(self, environ, start_response): if environ.get('HTTP_UPGRADE') is None: return parent.serve_static(environ, start_response) return super(Mixed, self).__call__(environ, start_response) - ws = make_server( - host, port, server_class=WSGIServer, - handler_class=WebSocketWSGIRequestHandler, - app=Mixed(handler_cls=LRSocket) - ) - ws.initialize_websockets_manager() - self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) - if browser: - if options['ipv6'] or '::' in host: - server_url = "http://[{0}]:{1}/".format(host, port) - else: - server_url = "http://{0}:{1}/".format(host, port) - - self.logger.info("Opening {0} in the default web browser...".format(server_url)) - # Yes, this is racy - webbrowser.open('http://{0}:{1}'.format(host, port)) - - try: - ws.serve_forever() - except KeyboardInterrupt: - self.logger.info("Server is shutting down.") - observer.stop() - observer.join() + if self.has_server: + ws = make_server( + host, port, server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=Mixed(handler_cls=LRSocket) + ) + ws.initialize_websockets_manager() + self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) + if browser: + if options['ipv6'] or '::' in host: + server_url = "http://[{0}]:{1}/".format(host, port) + else: + server_url = "http://{0}:{1}/".format(host, port) + + self.logger.info("Opening {0} in the default web browser...".format(server_url)) + # Yes, this is racy + webbrowser.open('http://{0}:{1}'.format(host, port)) + + try: + ws.serve_forever() + except KeyboardInterrupt: + self.logger.info("Server is shutting down.") + # This is a hack, but something is locking up in a futex + # and exit() doesn't work. + os.kill(os.getpid(), 15) + else: + # Workaround: can’t have nothing running (instant exit) + # but also can’t join threads (no way to exit) + # The joys of threading. + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + self.logger.info("Shutting down.") + # This is a hack, but something is locking up in a futex + # and exit() doesn't work. + os.kill(os.getpid(), 15) def do_rebuild(self, event): - self.logger.info('REBUILDING SITE (from {0})'.format(event.src_path)) + """Rebuild the site.""" + # Move events have a dest_path, some editors like gedit use a + # move on larger save operations for write protection + event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path + fname = os.path.basename(event_path) + if (fname.endswith('~') or + fname.startswith('.') or + os.path.isdir(event_path)): # Skip on folders, these are usually duplicates + return + self.logger.info('REBUILDING SITE (from {0})'.format(event_path)) p = subprocess.Popen(self.cmd_arguments, stderr=subprocess.PIPE) + error = p.stderr.read() + errord = error.decode('utf-8') if p.wait() != 0: - error = p.stderr.read() - self.logger.error(error) - error_signal.send(error=error) + self.logger.error(errord) + error_signal.send(error=errord) else: - error = p.stderr.read() - print(error) + print(errord) def do_refresh(self, event): - self.logger.info('REFRESHING: {0}'.format(event.src_path)) - p = os.path.relpath(event.src_path, os.path.abspath(self.site.config['OUTPUT_FOLDER'])) + """Refresh the page.""" + # Move events have a dest_path, some editors like gedit use a + # move on larger save operations for write protection + event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path + self.logger.info('REFRESHING: {0}'.format(event_path)) + p = os.path.relpath(event_path, os.path.abspath(self.site.config['OUTPUT_FOLDER'])) refresh_signal.send(path=p) def serve_static(self, environ, start_response): """Trivial static file server.""" uri = wsgiref.util.request_uri(environ) p_uri = urlparse(uri) - f_path = os.path.join(self.site.config['OUTPUT_FOLDER'], *p_uri.path.split('/')) - mimetype = mimetypes.guess_type(uri)[0] or 'text/html' + f_path = os.path.join(self.site.config['OUTPUT_FOLDER'], *[unquote(x) for x in p_uri.path.split('/')]) + + # ‘Pretty’ URIs and root are assumed to be HTML + mimetype = 'text/html' if uri.endswith('/') else mimetypes.guess_type(uri)[0] or 'application/octet-stream' if os.path.isdir(f_path): + if not f_path.endswith('/'): # Redirect to avoid breakage + start_response('301 Redirect', [('Location', p_uri.path + '/')]) + return [] f_path = os.path.join(f_path, self.site.config['INDEX_FILE']) + mimetype = 'text/html' if p_uri.path == '/robots.txt': start_response('200 OK', [('Content-type', 'text/plain')]) - return ['User-Agent: *\nDisallow: /\n'] + return ['User-Agent: *\nDisallow: /\n'.encode('utf-8')] elif os.path.isfile(f_path): with open(f_path, 'rb') as fd: start_response('200 OK', [('Content-type', mimetype)]) - return [self.inject_js(mimetype, fd.read())] + return [self.file_filter(mimetype, fd.read())] elif p_uri.path == '/livereload.js': with open(LRJS_PATH, 'rb') as fd: start_response('200 OK', [('Content-type', mimetype)]) - return [self.inject_js(mimetype, fd.read())] + return [self.file_filter(mimetype, fd.read())] start_response('404 ERR', []) - return [self.inject_js('text/html', ERROR_N.format(404).format(uri))] + return [self.file_filter('text/html', ERROR_N.format(404).format(uri).encode('utf-8'))] - def inject_js(self, mimetype, data): - """Inject livereload.js in HTML files.""" + def file_filter(self, mimetype, data): + """Apply necessary changes to document before serving.""" if mimetype == 'text/html': - data = re.sub('</head>', self.snippet, data.decode('utf8'), 1, re.IGNORECASE) + data = data.decode('utf8') + data = self.remove_base_tag(data) + data = self.inject_js(data) data = data.encode('utf8') return data + def inject_js(self, data): + """Inject livereload.js.""" + data = re.sub('</head>', self.snippet, data, 1, re.IGNORECASE) + return data + + def remove_base_tag(self, data): + """Comment out any <base> to allow local resolution of relative URLs.""" + data = re.sub(r'<base\s([^>]*)>', '<!--base \g<1>-->', data, re.IGNORECASE) + return data + pending = [] class LRSocket(WebSocket): + """Speak Livereload protocol.""" def __init__(self, *a, **kw): + """Initialize protocol handler.""" refresh_signal.connect(self.notify) error_signal.connect(self.send_error) super(LRSocket, self).__init__(*a, **kw) def received_message(self, message): + """Handle received message.""" message = json.loads(message.data.decode('utf8')) self.logger.info('<--- {0}'.format(message)) response = None @@ -364,3 +436,25 @@ class ConfigWatchHandler(FileSystemEventHandler): """Call the provided function on any event.""" if event._src_path == self.configuration_filename: self.function(event) + + +try: + # Monkeypatch to hide Broken Pipe Errors + f = WebSocketWSGIHandler.finish_response + + if sys.version_info[0] == 3: + EX = BrokenPipeError # NOQA + else: + EX = IOError + + def finish_response(self): + """Monkeypatched finish_response that ignores broken pipes.""" + try: + f(self) + except EX: # Client closed the connection, not a real error + pass + + WebSocketWSGIHandler.finish_response = finish_response +except NameError: + # In case there is no WebSocketWSGIHandler because of a failed import. + pass diff --git a/nikola/plugins/command/bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin index b428da3..fc25045 100644 --- a/nikola/plugins/command/bootswatch_theme.plugin +++ b/nikola/plugins/command/bootswatch_theme.plugin @@ -1,10 +1,13 @@ [Core] -Name = bootswatch_theme -Module = bootswatch_theme +name = bootswatch_theme +module = bootswatch_theme [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Given a swatch name and a parent theme, creates a custom theme. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Given a swatch name and a parent theme, creates a custom theme. + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py index e19c937..b5644a1 100644 --- a/nikola/plugins/command/bootswatch_theme.py +++ b/nikola/plugins/command/bootswatch_theme.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" + from __future__ import print_function import os import requests @@ -35,6 +37,7 @@ LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER) class CommandBootswatchTheme(Command): + """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" name = "bootswatch_theme" diff --git a/nikola/plugins/command/check.plugin b/nikola/plugins/command/check.plugin index dd0980e..e380e64 100644 --- a/nikola/plugins/command/check.plugin +++ b/nikola/plugins/command/check.plugin @@ -1,10 +1,13 @@ [Core] -Name = check -Module = check +name = check +module = check [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Check the generated site +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Check the generated site + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py index a9bc44a..abf183e 100644 --- a/nikola/plugins/command/check.py +++ b/nikola/plugins/command/check.py @@ -24,11 +24,14 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Check the generated site.""" + from __future__ import print_function from collections import defaultdict import os import re import sys +import time try: from urllib import unquote from urlparse import urlparse, urljoin, urldefrag @@ -40,7 +43,7 @@ import lxml.html import requests from nikola.plugin_categories import Command -from nikola.utils import get_logger +from nikola.utils import get_logger, STDERR_HANDLER def _call_nikola_list(site): @@ -58,6 +61,7 @@ def _call_nikola_list(site): def real_scan_files(site): + """Scan for files.""" task_fnames = set([]) real_fnames = set([]) output_folder = site.config['OUTPUT_FOLDER'] @@ -80,7 +84,8 @@ def real_scan_files(site): def fs_relpath_from_url_path(url_path): - """Expects as input an urlparse(s).path""" + """Create a filesystem relative path from an URL path.""" + # Expects as input an urlparse(s).path url_path = unquote(url_path) # in windows relative paths don't begin with os.sep if sys.platform == 'win32' and len(url_path): @@ -89,6 +94,7 @@ def fs_relpath_from_url_path(url_path): class CommandCheck(Command): + """Check the generated site.""" name = "check" @@ -147,7 +153,7 @@ class CommandCheck(Command): def _execute(self, options, args): """Check the generated site.""" - self.logger = get_logger('check', self.site.loghandlers) + self.logger = get_logger('check', STDERR_HANDLER) if not options['links'] and not options['files'] and not options['clean']: print(self.help()) @@ -169,6 +175,7 @@ class CommandCheck(Command): checked_remote_targets = {} def analyze(self, fname, find_sources=False, check_remote=False): + """Analyze links on a page.""" rv = False self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']] base_url = urlparse(self.site.config['BASE_URL']) @@ -217,15 +224,45 @@ class CommandCheck(Command): if parsed.netloc == base_url.netloc: # absolute URL to self.site continue if target in self.checked_remote_targets: # already checked this exact target - if self.checked_remote_targets[target] > 399: - self.logger.warn("Broken link in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) + if self.checked_remote_targets[target] in [301, 307]: + self.logger.warn("Remote link PERMANENTLY redirected in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) + elif self.checked_remote_targets[target] in [302, 308]: + self.logger.info("Remote link temporarily redirected in {1}: {2} [HTTP: {3}]".format(filename, target, self.checked_remote_targets[target])) + elif self.checked_remote_targets[target] > 399: + self.logger.error("Broken link in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) continue + + # Skip whitelisted targets + if any(re.search(_, target) for _ in self.whitelist): + continue + # Check the remote link works req_headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0 (Nikola)'} # I’m a real boy! - resp = requests.head(target, headers=req_headers) - self.checked_remote_targets[target] = resp.status_code + resp = requests.head(target, headers=req_headers, allow_redirects=False) + + # Retry client errors (4xx) as GET requests because many servers are broken + if resp.status_code >= 400 and resp.status_code <= 499: + time.sleep(0.5) + resp = requests.get(target, headers=req_headers, allow_redirects=False) + + # Follow redirects and see where they lead, redirects to errors will be reported twice + if resp.status_code in [301, 302, 307, 308]: + redir_status_code = resp.status_code + time.sleep(0.5) + # Known redirects are retested using GET because IIS servers otherwise get HEADaches + resp = requests.get(target, headers=req_headers, allow_redirects=True) + # Permanent redirects should be updated + if redir_status_code in [301, 308]: + self.logger.warn("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code)) + if redir_status_code in [302, 307]: + self.logger.info("Remote link temporarily redirected to \"{0}\" in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code)) + self.checked_remote_targets[resp.url] = resp.status_code + self.checked_remote_targets[target] = redir_status_code + else: + self.checked_remote_targets[target] = resp.status_code + if resp.status_code > 399: # Error - self.logger.warn("Broken link in {0}: {1} [Error {2}]".format(filename, target, resp.status_code)) + self.logger.error("Broken link in {0}: {1} [Error {2}]".format(filename, target, resp.status_code)) continue elif resp.status_code <= 399: # The address leads *somewhere* that is not an error self.logger.debug("Successfully checked remote link in {0}: {1} [HTTP: {2}]".format(filename, target, resp.status_code)) @@ -271,6 +308,7 @@ class CommandCheck(Command): return rv def scan_links(self, find_sources=False, check_remote=False): + """Check links on the site.""" self.logger.info("Checking Links:") self.logger.info("===============\n") self.logger.notice("{0} mode".format(self.site.config['URL_TYPE'])) @@ -286,6 +324,7 @@ class CommandCheck(Command): return failure def scan_files(self): + """Check files in the site, find missing and orphaned files.""" failure = False self.logger.info("Checking Files:") self.logger.info("===============\n") @@ -311,7 +350,22 @@ class CommandCheck(Command): return failure def clean_files(self): + """Remove orphaned files.""" only_on_output, _ = real_scan_files(self.site) for f in only_on_output: + self.logger.info('removed: {0}'.format(f)) os.unlink(f) + + # Find empty directories and remove them + output_folder = self.site.config['OUTPUT_FOLDER'] + all_dirs = [] + for root, dirs, files in os.walk(output_folder, followlinks=True): + all_dirs.append(root) + all_dirs.sort(key=len, reverse=True) + for d in all_dirs: + try: + os.rmdir(d) + self.logger.info('removed: {0}/'.format(d)) + except OSError: + pass return True diff --git a/nikola/plugins/command/console.plugin b/nikola/plugins/command/console.plugin index 3aef2e7..333762c 100644 --- a/nikola/plugins/command/console.plugin +++ b/nikola/plugins/command/console.plugin @@ -1,9 +1,13 @@ [Core] -Name = console -Module = console +name = console +module = console [Documentation] -Author = Chris Warrick, Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Start a debugging python console +author = Chris Warrick, Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Start a debugging python console + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py index b8e7825..539fa08 100644 --- a/nikola/plugins/command/console.py +++ b/nikola/plugins/command/console.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Start debugging console.""" + from __future__ import print_function, unicode_literals import os @@ -36,7 +38,9 @@ LOGGER = get_logger('console', STDERR_HANDLER) class CommandConsole(Command): + """Start debugging console.""" + name = "console" shells = ['ipython', 'bpython', 'plain'] doc_purpose = "start an interactive Python console with access to your site" diff --git a/nikola/plugins/command/deploy.plugin b/nikola/plugins/command/deploy.plugin index 14fd53f..4743ca2 100644 --- a/nikola/plugins/command/deploy.plugin +++ b/nikola/plugins/command/deploy.plugin @@ -1,9 +1,13 @@ [Core] -Name = deploy -Module = deploy +name = deploy +module = deploy [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Deploy the site +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Deploy the site + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py index 2c44e87..821ea11 100644 --- a/nikola/plugins/command/deploy.py +++ b/nikola/plugins/command/deploy.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Deploy site.""" + from __future__ import print_function import io from datetime import datetime @@ -35,11 +37,13 @@ import time from blinker import signal from nikola.plugin_categories import Command -from nikola.utils import get_logger, remove_file, unicode_str, makedirs +from nikola.utils import get_logger, remove_file, unicode_str, makedirs, STDERR_HANDLER class CommandDeploy(Command): + """Deploy site.""" + name = "deploy" doc_usage = "[[preset [preset...]]" @@ -48,7 +52,8 @@ class CommandDeploy(Command): logger = None def _execute(self, command, args): - self.logger = get_logger('deploy', self.site.loghandlers) + """Execute the deploy command.""" + self.logger = get_logger('deploy', STDERR_HANDLER) # Get last successful deploy date timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy') if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo': @@ -116,7 +121,7 @@ class CommandDeploy(Command): outf.write(unicode_str(new_deploy.isoformat())) def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None): - """ Emit events for all timeline entries newer than last deploy. + """Emit events for all timeline entries newer than last deploy. last_deploy: datetime Time stamp of the last successful deployment. @@ -128,7 +133,6 @@ class CommandDeploy(Command): True when it appears like deploy is being run after a clean. """ - event = { 'last_deploy': last_deploy, 'new_deploy': new_deploy, diff --git a/nikola/plugins/command/github_deploy.plugin b/nikola/plugins/command/github_deploy.plugin index 74e7902..e793548 100644 --- a/nikola/plugins/command/github_deploy.plugin +++ b/nikola/plugins/command/github_deploy.plugin @@ -1,9 +1,13 @@ [Core] -Name = github_deploy -Module = github_deploy +name = github_deploy +module = github_deploy [Documentation] -Author = Puneeth Chaganti -Version = 1,0 -Website = http://getnikola.com -Description = Deploy the site to GitHub pages. +author = Puneeth Chaganti +version = 1,0 +website = http://getnikola.com +description = Deploy the site to GitHub pages. + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py index 888a4f9..0ab9332 100644 --- a/nikola/plugins/command/github_deploy.py +++ b/nikola/plugins/command/github_deploy.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Deploy site to GitHub Pages.""" + from __future__ import print_function from datetime import datetime import io @@ -33,17 +35,19 @@ from textwrap import dedent from nikola.plugin_categories import Command from nikola.plugins.command.check import real_scan_files -from nikola.utils import get_logger, req_missing, makedirs, unicode_str +from nikola.utils import get_logger, req_missing, makedirs, unicode_str, STDERR_HANDLER from nikola.__main__ import main from nikola import __version__ def uni_check_output(*args, **kwargs): + """Run command and return output as Unicode (UTf-8).""" o = subprocess.check_output(*args, **kwargs) return o.decode('utf-8') def check_ghp_import_installed(): + """Check if ghp-import is installed.""" try: subprocess.check_output(['ghp-import', '-h']) except OSError: @@ -53,7 +57,9 @@ def check_ghp_import_installed(): class CommandGitHubDeploy(Command): - """ Deploy site to GitHub Pages. """ + + """Deploy site to GitHub Pages.""" + name = 'github_deploy' doc_usage = '' @@ -70,10 +76,8 @@ class CommandGitHubDeploy(Command): logger = None def _execute(self, command, args): - - self.logger = get_logger( - CommandGitHubDeploy.name, self.site.loghandlers - ) + """Run the deployment.""" + self.logger = get_logger(CommandGitHubDeploy.name, STDERR_HANDLER) # Check if ghp-import is installed check_ghp_import_installed() @@ -95,8 +99,7 @@ class CommandGitHubDeploy(Command): return def _commit_and_push(self): - """ Commit all the files and push. """ - + """Commit all the files and push.""" source = self.site.config['GITHUB_SOURCE_BRANCH'] deploy = self.site.config['GITHUB_DEPLOY_BRANCH'] remote = self.site.config['GITHUB_REMOTE_NAME'] diff --git a/nikola/plugins/command/import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin index e072224..6c4384e 100644 --- a/nikola/plugins/command/import_wordpress.plugin +++ b/nikola/plugins/command/import_wordpress.plugin @@ -1,10 +1,13 @@ [Core] -Name = import_wordpress -Module = import_wordpress +name = import_wordpress +module = import_wordpress [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Import a wordpress site from a XML dump (requires markdown). +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Import a wordpress site from a XML dump (requires markdown). + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py index 674fc2a..a652ec8 100644 --- a/nikola/plugins/command/import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -24,13 +24,18 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Import a WordPress dump.""" + from __future__ import unicode_literals, print_function import os import re import sys import datetime +import io +import json import requests from lxml import etree +from collections import defaultdict try: from urlparse import urlparse @@ -53,7 +58,37 @@ from nikola.plugins.command.init import SAMPLE_CONF, prepare_config, format_defa LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) +def install_plugin(site, plugin_name, output_dir=None, show_install_notes=False): + """Install a Nikola plugin.""" + LOGGER.notice("Installing plugin '{0}'".format(plugin_name)) + # Get hold of the 'plugin' plugin + plugin_installer_info = site.plugin_manager.getPluginByName('plugin', 'Command') + if plugin_installer_info is None: + LOGGER.error('Internal error: cannot find the "plugin" plugin which is supposed to come with Nikola!') + return False + if not plugin_installer_info.is_activated: + # Someone might have disabled the plugin in the `conf.py` used + site.plugin_manager.activatePluginByName(plugin_installer_info.name) + plugin_installer_info.plugin_object.set_site(site) + plugin_installer = plugin_installer_info.plugin_object + # Try to install the requested plugin + options = {} + for option in plugin_installer.cmd_options: + options[option['name']] = option['default'] + options['install'] = plugin_name + options['output_dir'] = output_dir + options['show_install_notes'] = show_install_notes + if plugin_installer.execute(options=options) > 0: + return False + # Let the plugin manager find newly installed plugins + site.plugin_manager.collectPlugins() + # Re-scan for compiler extensions + site.compiler_extensions = site._activate_plugins_of_category("CompilerExtension") + return True + + class CommandImportWordpress(Command, ImportMixin): + """Import a WordPress dump.""" name = "import_wordpress" @@ -70,6 +105,20 @@ class CommandImportWordpress(Command, ImportMixin): 'help': "Don't import drafts", }, { + 'name': 'exclude_privates', + 'long': 'exclude-privates', + 'default': False, + 'type': bool, + 'help': "Don't import private posts", + }, + { + 'name': 'include_empty_items', + 'long': 'include-empty-items', + 'default': False, + 'type': bool, + 'help': "Include empty posts and pages", + }, + { 'name': 'squash_newlines', 'long': 'squash-newlines', 'default': False, @@ -107,15 +156,57 @@ class CommandImportWordpress(Command, ImportMixin): 'type': str, 'help': "The pattern for translation files names", }, + { + 'name': 'export_categories_as_categories', + 'long': 'export-categories-as-categories', + 'default': False, + 'type': bool, + 'help': "Export categories as categories, instead of treating them as tags", + }, + { + 'name': 'export_comments', + 'long': 'export-comments', + 'default': False, + 'type': bool, + 'help': "Export comments as .wpcomment files", + }, + { + 'name': 'transform_to_html', + 'long': 'transform-to-html', + 'default': False, + 'type': bool, + 'help': "Uses WordPress page compiler to transform WordPress posts directly to HTML during import", + }, + { + 'name': 'use_wordpress_compiler', + 'long': 'use-wordpress-compiler', + 'default': False, + 'type': bool, + 'help': "Instead of converting posts to markdown, leave them as is and use the WordPress page compiler", + }, + { + 'name': 'install_wordpress_compiler', + 'long': 'install-wordpress-compiler', + 'default': False, + 'type': bool, + 'help': "Automatically installs the WordPress page compiler (either locally or in the new site) if required by other options.\nWarning: the compiler is GPL software!", + }, ] all_tags = set([]) - def _execute(self, options={}, args=[]): - """Import a WordPress blog from an export file into a Nikola site.""" - if not args: - print(self.help()) + def _find_wordpress_compiler(self): + """Find WordPress compiler plugin.""" + if self.wordpress_page_compiler is not None: return - + plugin_info = self.site.plugin_manager.getPluginByName('wordpress', 'PageCompiler') + if plugin_info is not None: + if not plugin_info.is_activated: + self.site.plugin_manager.activatePluginByName(plugin_info.name) + plugin_info.plugin_object.set_site(self.site) + self.wordpress_page_compiler = plugin_info.plugin_object + + def _read_options(self, options, args): + """Read command-line options.""" options['filename'] = args.pop(0) if args and ('output_folder' not in args or @@ -136,19 +227,76 @@ class CommandImportWordpress(Command, ImportMixin): self.output_folder = options.get('output_folder', 'new_site') self.exclude_drafts = options.get('exclude_drafts', False) + self.exclude_privates = options.get('exclude_privates', False) self.no_downloads = options.get('no_downloads', False) + self.import_empty_items = options.get('include_empty_items', False) + + self.export_categories_as_categories = options.get('export_categories_as_categories', False) + self.export_comments = options.get('export_comments', False) + + self.transform_to_html = options.get('transform_to_html', False) + self.use_wordpress_compiler = options.get('use_wordpress_compiler', False) + self.install_wordpress_compiler = options.get('install_wordpress_compiler', False) + self.wordpress_page_compiler = None self.auth = None if options.get('download_auth') is not None: username_password = options.get('download_auth') self.auth = tuple(username_password.split(':', 1)) if len(self.auth) < 2: - print("Please specify HTTP authentication credentials in the form username:password.") + LOGGER.error("Please specify HTTP authentication credentials in the form username:password.") return False self.separate_qtranslate_content = options.get('separate_qtranslate_content') self.translations_pattern = options.get('translations_pattern') + if self.transform_to_html and self.use_wordpress_compiler: + LOGGER.warn("It does not make sense to combine --transform-to-html with --use-wordpress-compiler, as the first converts all posts to HTML and the latter option affects zero posts.") + + if self.transform_to_html: + self._find_wordpress_compiler() + if not self.wordpress_page_compiler and self.install_wordpress_compiler: + if not install_plugin(self.site, 'wordpress_compiler', output_dir='plugins'): # local install + return False + self._find_wordpress_compiler() + if not self.wordpress_page_compiler: + LOGGER.error("To compile WordPress posts to HTML, the WordPress post compiler is needed. You can install it via:") + LOGGER.error(" nikola plugin -i wordpress_compiler") + LOGGER.error("Please note that the WordPress post compiler is licensed under the GPL v2.") + return False + + return True + + def _prepare(self, channel): + """Prepare context and category hierarchy.""" + self.context = self.populate_context(channel) + self.base_dir = urlparse(self.context['BASE_URL']).path + + if self.export_categories_as_categories: + wordpress_namespace = channel.nsmap['wp'] + cat_map = dict() + for cat in channel.findall('{{{0}}}category'.format(wordpress_namespace)): + # cat_id = get_text_tag(cat, '{{{0}}}term_id'.format(wordpress_namespace), None) + cat_slug = get_text_tag(cat, '{{{0}}}category_nicename'.format(wordpress_namespace), None) + cat_parent_slug = get_text_tag(cat, '{{{0}}}category_parent'.format(wordpress_namespace), None) + cat_name = get_text_tag(cat, '{{{0}}}cat_name'.format(wordpress_namespace), None) + cat_path = [cat_name] + if cat_parent_slug in cat_map: + cat_path = cat_map[cat_parent_slug] + cat_path + cat_map[cat_slug] = cat_path + self._category_paths = dict() + for cat, path in cat_map.items(): + self._category_paths[cat] = utils.join_hierarchical_category_path(path) + + def _execute(self, options={}, args=[]): + """Import a WordPress blog from an export file into a Nikola site.""" + if not args: + print(self.help()) + return False + + if not self._read_options(options, args): + return False + # A place holder where extra language (if detected) will be stored self.extra_languages = set() @@ -166,8 +314,7 @@ class CommandImportWordpress(Command, ImportMixin): req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads') channel = self.get_channel_from_file(self.wordpress_export_file) - self.context = self.populate_context(channel) - self.base_dir = urlparse(self.context['BASE_URL']).path + self._prepare(channel) conf_template = self.generate_base_site() # If user has specified a custom pattern for translation files we @@ -181,6 +328,11 @@ class CommandImportWordpress(Command, ImportMixin): self.extra_languages) self.context['REDIRECTIONS'] = self.configure_redirections( self.url_map) + if self.timezone: + self.context['TIMEZONE'] = self.timezone + if self.export_categories_as_categories: + self.context['CATEGORY_ALLOW_HIERARCHIES'] = True + self.context['CATEGORY_OUTPUT_FLAT_HIERARCHY'] = True # Add tag redirects for tag in self.all_tags: @@ -197,18 +349,21 @@ class CommandImportWordpress(Command, ImportMixin): self.write_urlmap_csv( os.path.join(self.output_folder, 'url_map.csv'), self.url_map) rendered_template = conf_template.render(**prepare_config(self.context)) - rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', - rendered_template) - - if self.timezone: - rendered_template = re.sub('# TIMEZONE = \'UTC\'', - 'TIMEZONE = \'' + self.timezone + '\'', - rendered_template) self.write_configuration(self.get_configuration_output_path(), rendered_template) + if self.use_wordpress_compiler: + if self.install_wordpress_compiler: + if not install_plugin(self.site, 'wordpress_compiler', output_dir=os.path.join(self.output_folder, 'plugins')): + return False + else: + LOGGER.warn("Make sure to install the WordPress page compiler via") + LOGGER.warn(" nikola plugin -i wordpress_compiler") + LOGGER.warn("in your imported blog's folder ({0}), if you haven't installed it system-wide or user-wide. Otherwise, your newly imported blog won't compile.".format(self.output_folder)) + @classmethod def read_xml_file(cls, filename): + """Read XML file into memory.""" xml = [] with open(filename, 'rb') as fd: @@ -221,12 +376,13 @@ class CommandImportWordpress(Command, ImportMixin): @classmethod def get_channel_from_file(cls, filename): + """Get channel from XML file.""" tree = etree.fromstring(cls.read_xml_file(filename)) channel = tree.find('channel') return channel - @staticmethod - def populate_context(channel): + def populate_context(self, channel): + """Populate context with config for the site.""" wordpress_namespace = channel.nsmap['wp'] context = SAMPLE_CONF.copy() @@ -255,28 +411,31 @@ class CommandImportWordpress(Command, ImportMixin): author, '{{{0}}}author_display_name'.format(wordpress_namespace), "Joe Example") - context['POSTS'] = '''( - ("posts/*.rst", "posts", "post.tmpl"), - ("posts/*.txt", "posts", "post.tmpl"), - ("posts/*.md", "posts", "post.tmpl"), - ("posts/*.wp", "posts", "post.tmpl"), - )''' - context['PAGES'] = '''( - ("stories/*.rst", "stories", "story.tmpl"), - ("stories/*.txt", "stories", "story.tmpl"), - ("stories/*.md", "stories", "story.tmpl"), - ("stories/*.wp", "stories", "story.tmpl"), - )''' - context['COMPILERS'] = '''{ - "rest": ('.txt', '.rst'), - "markdown": ('.md', '.mdown', '.markdown', '.wp'), - "html": ('.html', '.htm') - } - ''' + extensions = ['rst', 'txt', 'md', 'html'] + if self.use_wordpress_compiler: + extensions.append('wp') + POSTS = '(\n' + PAGES = '(\n' + for extension in extensions: + POSTS += ' ("posts/*.{0}", "posts", "post.tmpl"),\n'.format(extension) + PAGES += ' ("stories/*.{0}", "stories", "story.tmpl"),\n'.format(extension) + POSTS += ')\n' + PAGES += ')\n' + context['POSTS'] = POSTS + context['PAGES'] = PAGES + COMPILERS = '{\n' + COMPILERS += ''' "rest": ('.txt', '.rst'),''' + '\n' + COMPILERS += ''' "markdown": ('.md', '.mdown', '.markdown'),''' + '\n' + COMPILERS += ''' "html": ('.html', '.htm'),''' + '\n' + if self.use_wordpress_compiler: + COMPILERS += ''' "wordpress": ('.wp'),''' + '\n' + COMPILERS += '}' + context['COMPILERS'] = COMPILERS return context def download_url_content_to_file(self, url, dst_path): + """Download some content (attachments) to a file.""" if self.no_downloads: return @@ -291,6 +450,8 @@ class CommandImportWordpress(Command, ImportMixin): LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err)) def import_attachment(self, item, wordpress_namespace): + """Import an attachment to the site.""" + # Download main image url = get_text_tag( item, '{{{0}}}attachment_url'.format(wordpress_namespace), 'foo') link = get_text_tag(item, '{{{0}}}link'.format(wordpress_namespace), @@ -305,59 +466,136 @@ class CommandImportWordpress(Command, ImportMixin): links[link] = '/' + dst_url links[url] = '/' + dst_url - self.download_additional_image_sizes( - item, - wordpress_namespace, - os.path.dirname(url) - ) - - def download_additional_image_sizes(self, item, wordpress_namespace, source_path): - if phpserialize is None: - return + files = [path] + files_meta = [{}] additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace)) - if additional_metadata is None: - return - - for element in additional_metadata: - meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace)) - if meta_key is not None and meta_key.text == '_wp_attachment_metadata': - meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace)) - - if meta_value is None: - continue - - # Someone from Wordpress thought it was a good idea - # serialize PHP objects into that metadata field. Given - # that the export should give you the power to insert - # your blogging into another site or system its not. - # Why don't they just use JSON? - if sys.version_info[0] == 2: - try: - metadata = phpserialize.loads(utils.sys_encode(meta_value.text)) - except ValueError: - # local encoding might be wrong sometimes + if phpserialize and additional_metadata: + source_path = os.path.dirname(url) + for element in additional_metadata: + meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace)) + if meta_key is not None and meta_key.text == '_wp_attachment_metadata': + meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace)) + + if meta_value is None: + continue + + # Someone from Wordpress thought it was a good idea + # serialize PHP objects into that metadata field. Given + # that the export should give you the power to insert + # your blogging into another site or system its not. + # Why don't they just use JSON? + if sys.version_info[0] == 2: + try: + metadata = phpserialize.loads(utils.sys_encode(meta_value.text)) + except ValueError: + # local encoding might be wrong sometimes + metadata = phpserialize.loads(meta_value.text.encode('utf-8')) + else: metadata = phpserialize.loads(meta_value.text.encode('utf-8')) - else: - metadata = phpserialize.loads(meta_value.text.encode('utf-8')) - size_key = b'sizes' - file_key = b'file' - - if size_key not in metadata: - continue - - for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]: - url = '/'.join([source_path, filename.decode('utf-8')]) - path = urlparse(url).path - dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) - dst_dir = os.path.dirname(dst_path) - utils.makedirs(dst_dir) - LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) - self.download_url_content_to_file(url, dst_path) - dst_url = '/'.join(dst_path.split(os.sep)[2:]) - links[url] = '/' + dst_url - links[url] = '/' + dst_url + meta_key = b'image_meta' + size_key = b'sizes' + file_key = b'file' + width_key = b'width' + height_key = b'height' + + # Extract metadata + if width_key in metadata and height_key in metadata: + files_meta[0]['width'] = int(metadata[width_key]) + files_meta[0]['height'] = int(metadata[height_key]) + + if meta_key in metadata: + image_meta = metadata[meta_key] + dst_meta = {} + + def add(our_key, wp_key, is_int=False, ignore_zero=False, is_float=False): + if wp_key in image_meta: + value = image_meta[wp_key] + if is_int: + value = int(value) + if ignore_zero and value == 0: + return + elif is_float: + value = float(value) + if ignore_zero and value == 0: + return + else: + value = value.decode('utf-8') # assume UTF-8 + if value == '': # skip empty values + return + dst_meta[our_key] = value + + add('aperture', b'aperture', is_float=True, ignore_zero=True) + add('credit', b'credit') + add('camera', b'camera') + add('caption', b'caption') + add('created_timestamp', b'created_timestamp', is_float=True, ignore_zero=True) + add('copyright', b'copyright') + add('focal_length', b'focal_length', is_float=True, ignore_zero=True) + add('iso', b'iso', is_float=True, ignore_zero=True) + add('shutter_speed', b'shutter_speed', ignore_zero=True, is_float=True) + add('title', b'title') + + if len(dst_meta) > 0: + files_meta[0]['meta'] = dst_meta + + # Find other sizes of image + if size_key not in metadata: + continue + + for size in metadata[size_key]: + filename = metadata[size_key][size][file_key] + url = '/'.join([source_path, filename.decode('utf-8')]) + + # Construct metadata + meta = {} + meta['size'] = size.decode('utf-8') + if width_key in metadata[size_key][size] and height_key in metadata[size_key][size]: + meta['width'] = metadata[size_key][size][width_key] + meta['height'] = metadata[size_key][size][height_key] + + path = urlparse(url).path + dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) + dst_dir = os.path.dirname(dst_path) + utils.makedirs(dst_dir) + LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) + self.download_url_content_to_file(url, dst_path) + dst_url = '/'.join(dst_path.split(os.sep)[2:]) + links[url] = '/' + dst_url + + files.append(path) + files_meta.append(meta) + + # Prepare result + result = {} + result['files'] = files + result['files_meta'] = files_meta + + # Prepare extraction of more information + dc_namespace = item.nsmap['dc'] + content_namespace = item.nsmap['content'] + excerpt_namespace = item.nsmap['excerpt'] + + def add(result_key, key, namespace=None, filter=None, store_empty=False): + if namespace is not None: + value = get_text_tag(item, '{{{0}}}{1}'.format(namespace, key), None) + else: + value = get_text_tag(item, key, None) + if value is not None: + if filter: + value = filter(value) + if value or store_empty: + result[result_key] = value + + add('title', 'title') + add('date_utc', 'post_date_gmt', namespace=wordpress_namespace) + add('wordpress_user_name', 'creator', namespace=dc_namespace) + add('content', 'encoded', namespace=content_namespace) + add('excerpt', 'encoded', namespace=excerpt_namespace) + add('description', 'description') + + return result code_re1 = re.compile(r'\[code.* lang.*?="(.*?)?".*\](.*?)\[/code\]', re.DOTALL | re.MULTILINE) code_re2 = re.compile(r'\[sourcecode.* lang.*?="(.*?)?".*\](.*?)\[/sourcecode\]', re.DOTALL | re.MULTILINE) @@ -365,6 +603,7 @@ class CommandImportWordpress(Command, ImportMixin): code_re4 = re.compile(r'\[sourcecode.*?\](.*?)\[/sourcecode\]', re.DOTALL | re.MULTILINE) def transform_code(self, content): + """Transform code blocks.""" # http://en.support.wordpress.com/code/posting-source-code/. There are # a ton of things not supported here. We only do a basic [code # lang="x"] -> ```x translation, and remove quoted html entities (<, @@ -390,26 +629,126 @@ class CommandImportWordpress(Command, ImportMixin): @staticmethod def transform_caption(content): + """Transform captions.""" new_caption = re.sub(r'\[/caption\]', '', content) new_caption = re.sub(r'\[caption.*\]', '', new_caption) return new_caption def transform_multiple_newlines(self, content): - """Replaces multiple newlines with only two.""" + """Replace multiple newlines with only two.""" if self.squash_newlines: return re.sub(r'\n{3,}', r'\n\n', content) else: return content - def transform_content(self, content): - content = self.transform_code(content) - content = self.transform_caption(content) - content = self.transform_multiple_newlines(content) - return content + def transform_content(self, content, post_format, attachments): + """Transform content into appropriate format.""" + if post_format == 'wp': + if self.transform_to_html: + additional_data = {} + if attachments is not None: + additional_data['attachments'] = attachments + try: + content = self.wordpress_page_compiler.compile_to_string(content, additional_data=additional_data) + except TypeError: # old versions of the plugin don't support the additional argument + content = self.wordpress_page_compiler.compile_to_string(content) + return content, 'html', True + elif self.use_wordpress_compiler: + return content, 'wp', False + else: + content = self.transform_code(content) + content = self.transform_caption(content) + content = self.transform_multiple_newlines(content) + return content, 'md', True + elif post_format == 'markdown': + return content, 'md', True + elif post_format == 'none': + return content, 'html', True + else: + return None + + def _extract_comment(self, comment, wordpress_namespace): + """Extract comment from dump.""" + id = int(get_text_tag(comment, "{{{0}}}comment_id".format(wordpress_namespace), None)) + author = get_text_tag(comment, "{{{0}}}comment_author".format(wordpress_namespace), None) + author_email = get_text_tag(comment, "{{{0}}}comment_author_email".format(wordpress_namespace), None) + author_url = get_text_tag(comment, "{{{0}}}comment_author_url".format(wordpress_namespace), None) + author_IP = get_text_tag(comment, "{{{0}}}comment_author_IP".format(wordpress_namespace), None) + # date = get_text_tag(comment, "{{{0}}}comment_date".format(wordpress_namespace), None) + date_gmt = get_text_tag(comment, "{{{0}}}comment_date_gmt".format(wordpress_namespace), None) + content = get_text_tag(comment, "{{{0}}}comment_content".format(wordpress_namespace), None) + approved = get_text_tag(comment, "{{{0}}}comment_approved".format(wordpress_namespace), '0') + if approved == '0': + approved = 'hold' + elif approved == '1': + approved = 'approved' + elif approved == 'spam' or approved == 'trash': + pass + else: + LOGGER.warn("Unknown comment approved status: " + str(approved)) + parent = int(get_text_tag(comment, "{{{0}}}comment_parent".format(wordpress_namespace), 0)) + if parent == 0: + parent = None + user_id = int(get_text_tag(comment, "{{{0}}}comment_user_id".format(wordpress_namespace), 0)) + if user_id == 0: + user_id = None + + if approved == 'trash' or approved == 'spam': + return None + + return {"id": id, "status": str(approved), "approved": approved == "approved", + "author": author, "email": author_email, "url": author_url, "ip": author_IP, + "date": date_gmt, "content": content, "parent": parent, "user_id": user_id} + + def _write_comment(self, filename, comment): + """Write comment to file.""" + def write_header_line(fd, header_field, header_content): + """Write comment header line.""" + if header_content is None: + return + header_content = str(header_content).replace('\n', ' ') + line = '.. ' + header_field + ': ' + header_content + '\n' + fd.write(line.encode('utf8')) + + with open(filename, "wb+") as fd: + write_header_line(fd, "id", comment["id"]) + write_header_line(fd, "status", comment["status"]) + write_header_line(fd, "approved", comment["approved"]) + write_header_line(fd, "author", comment["author"]) + write_header_line(fd, "author_email", comment["email"]) + write_header_line(fd, "author_url", comment["url"]) + write_header_line(fd, "author_IP", comment["ip"]) + write_header_line(fd, "date_utc", comment["date"]) + write_header_line(fd, "parent_id", comment["parent"]) + write_header_line(fd, "wordpress_user_id", comment["user_id"]) + fd.write(('\n' + comment['content']).encode('utf8')) + + def _create_metadata(self, status, excerpt, tags, categories, post_name=None): + """Create post metadata.""" + other_meta = {'wp-status': status} + if excerpt is not None: + other_meta['excerpt'] = excerpt + if self.export_categories_as_categories: + cats = [] + for text in categories: + if text in self._category_paths: + cats.append(self._category_paths[text]) + else: + cats.append(utils.join_hierarchical_category_path([text])) + other_meta['categories'] = ','.join(cats) + if len(cats) > 0: + other_meta['category'] = cats[0] + if len(cats) > 1: + LOGGER.warn(('Post "{0}" has more than one category! ' + + 'Will only use the first one.').format(post_name)) + tags_cats = tags + else: + tags_cats = tags + categories + return tags_cats, other_meta - def import_item(self, item, wordpress_namespace, out_folder=None): - """Takes an item from the feed and creates a post file.""" + def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attachments=None): + """Take an item from the feed and creates a post file.""" if out_folder is None: out_folder = 'posts' @@ -439,7 +778,7 @@ class CommandImportWordpress(Command, ImportMixin): item, '{{{0}}}post_id'.format(wordpress_namespace), None) if not slug: # should never happen LOGGER.error("Error converting post:", title) - return + return False else: if len(pathlist) > 1: out_folder = os.path.join(*([out_folder] + pathlist[:-1])) @@ -461,23 +800,42 @@ class CommandImportWordpress(Command, ImportMixin): item, '{{{0}}}status'.format(wordpress_namespace), 'publish') content = get_text_tag( item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') + excerpt = get_text_tag( + item, '{http://wordpress.org/export/1.2/excerpt/}encoded', None) + + if excerpt is not None: + if len(excerpt) == 0: + excerpt = None tags = [] + categories = [] if status == 'trash': LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title)) - return + return False + elif status == 'private': + tags.append('private') + is_draft = False + is_private = True elif status != 'publish': tags.append('draft') is_draft = True + is_private = False else: is_draft = False + is_private = False for tag in item.findall('category'): text = tag.text - if text == 'Uncategorized': + type = 'category' + if 'domain' in tag.attrib: + type = tag.attrib['domain'] + if text == 'Uncategorized' and type == 'category': continue - tags.append(text) self.all_tags.add(text) + if type == 'category': + categories.append(type) + else: + tags.append(text) if '$latex' in content: tags.append('mathjax') @@ -487,11 +845,16 @@ class CommandImportWordpress(Command, ImportMixin): format_tag = [x for x in item.findall('*//{%s}meta_key' % wordpress_namespace) if x.text == '_tc_post_format'] if format_tag: post_format = format_tag[0].getparent().find('{%s}meta_value' % wordpress_namespace).text + if post_format == 'wpautop': + post_format = 'wp' if is_draft and self.exclude_drafts: LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) - - elif content.strip(): + return False + elif is_private and self.exclude_privates: + LOGGER.notice('Private post "{0}" will not be imported.'.format(title)) + return False + elif content.strip() or self.import_empty_items: # If no content is found, no files are written. self.url_map[link] = (self.context['SITE_URL'] + out_folder.rstrip('/') + '/' + slug + @@ -503,53 +866,121 @@ class CommandImportWordpress(Command, ImportMixin): content_translations = {"": content} default_language = self.context["DEFAULT_LANG"] for lang, content in content_translations.items(): + try: + content, extension, rewrite_html = self.transform_content(content, post_format, attachments) + except: + LOGGER.error(('Cannot interpret post "{0}" (language {1}) with post ' + + 'format {2}!').format(os.path.join(out_folder, slug), lang, post_format)) + return False if lang: out_meta_filename = slug + '.meta' if lang == default_language: - out_content_filename = slug + '.wp' + out_content_filename = slug + '.' + extension else: out_content_filename \ = utils.get_translation_candidate(self.context, - slug + ".wp", lang) + slug + "." + extension, lang) self.extra_languages.add(lang) meta_slug = slug else: out_meta_filename = slug + '.meta' - out_content_filename = slug + '.wp' + out_content_filename = slug + '.' + extension meta_slug = slug - if post_format == 'wp': - content = self.transform_content(content) + tags, other_meta = self._create_metadata(status, excerpt, tags, categories, + post_name=os.path.join(out_folder, slug)) self.write_metadata(os.path.join(self.output_folder, out_folder, out_meta_filename), - title, meta_slug, post_date, description, tags) + title, meta_slug, post_date, description, tags, **other_meta) self.write_content( os.path.join(self.output_folder, out_folder, out_content_filename), - content) + content, + rewrite_html) + + if self.export_comments: + comments = [] + for tag in item.findall('{{{0}}}comment'.format(wordpress_namespace)): + comment = self._extract_comment(tag, wordpress_namespace) + if comment is not None: + comments.append(comment) + + for comment in comments: + comment_filename = slug + "." + str(comment['id']) + ".wpcomment" + self._write_comment(os.path.join(self.output_folder, out_folder, comment_filename), comment) + + return (out_folder, slug) else: - LOGGER.warn('Not going to import "{0}" because it seems to contain' - ' no content.'.format(title)) + LOGGER.warn(('Not going to import "{0}" because it seems to contain' + ' no content.').format(title)) + return False - def process_item(self, item): + def _extract_item_info(self, item): + """Extract information about an item.""" # The namespace usually is something like: # http://wordpress.org/export/1.2/ wordpress_namespace = item.nsmap['wp'] post_type = get_text_tag( item, '{{{0}}}post_type'.format(wordpress_namespace), 'post') + post_id = int(get_text_tag( + item, '{{{0}}}post_id'.format(wordpress_namespace), "0")) + parent_id = get_text_tag( + item, '{{{0}}}post_parent'.format(wordpress_namespace), None) + return wordpress_namespace, post_type, post_id, parent_id + + def process_item_if_attachment(self, item): + """Process attachments.""" + wordpress_namespace, post_type, post_id, parent_id = self._extract_item_info(item) if post_type == 'attachment': - self.import_attachment(item, wordpress_namespace) - elif post_type == 'post': - self.import_item(item, wordpress_namespace, 'posts') - else: - self.import_item(item, wordpress_namespace, 'stories') + data = self.import_attachment(item, wordpress_namespace) + # If parent was found, store relation with imported files + if parent_id is not None and int(parent_id) != 0: + self.attachments[int(parent_id)][post_id] = data + else: + LOGGER.warn("Attachment #{0} ({1}) has no parent!".format(post_id, data['files'])) + + def write_attachments_info(self, path, attachments): + """Write attachments info file.""" + with io.open(path, "wb") as file: + file.write(json.dumps(attachments).encode('utf-8')) + + def process_item_if_post_or_page(self, item): + """Process posts and pages.""" + wordpress_namespace, post_type, post_id, parent_id = self._extract_item_info(item) + + if post_type != 'attachment': + # Get attachments for post + attachments = self.attachments.pop(post_id, None) + # Import item + if post_type == 'post': + out_folder_slug = self.import_postpage_item(item, wordpress_namespace, 'posts', attachments) + else: + out_folder_slug = self.import_postpage_item(item, wordpress_namespace, 'stories', attachments) + # Process attachment data + if attachments is not None: + # If post was exported, store data + if out_folder_slug: + destination = os.path.join(self.output_folder, out_folder_slug[0], + out_folder_slug[1] + ".attachments.json") + self.write_attachments_info(destination, attachments) def import_posts(self, channel): + """Import posts into the site.""" + self.attachments = defaultdict(dict) + # First process attachments + for item in channel.findall('item'): + self.process_item_if_attachment(item) + # Next process posts for item in channel.findall('item'): - self.process_item(item) + self.process_item_if_post_or_page(item) + # Assign attachments to posts + for post_id in self.attachments: + LOGGER.warn(("Found attachments for post or page #{0}, but didn't find post or page. " + + "(Attachments: {1})").format(post_id, [e['files'][0] for e in self.attachments[post_id].values()])) def get_text_tag(tag, name, default): + """Get the text of an XML tag.""" if tag is None: return default t = tag.find(name) @@ -560,9 +991,10 @@ def get_text_tag(tag, name, default): def separate_qtranslate_content(text): - """Parse the content of a wordpress post or page and separate - the various language specific contents when they are delimited - with qtranslate tags: <!--:LL-->blabla<!--:-->""" + """Parse the content of a wordpress post or page and separate qtranslate languages. + + qtranslate tags: <!--:LL-->blabla<!--:--> + """ # TODO: uniformize qtranslate tags <!--/en--> => <!--:--> qt_start = "<!--:" qt_end = "-->" diff --git a/nikola/plugins/command/init.plugin b/nikola/plugins/command/init.plugin index 850dba9..a5404c4 100644 --- a/nikola/plugins/command/init.plugin +++ b/nikola/plugins/command/init.plugin @@ -1,9 +1,13 @@ [Core] -Name = init -Module = init +name = init +module = init [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create a new site. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create a new site. + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py index 7a36894..91ccdb4 100644 --- a/nikola/plugins/command/init.py +++ b/nikola/plugins/command/init.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Create a new site.""" + from __future__ import print_function, unicode_literals import os import shutil @@ -54,6 +56,7 @@ SAMPLE_CONF = { 'BLOG_EMAIL': "joe@demo.site", 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", 'PRETTY_URLS': False, + 'STRIP_INDEXES': False, 'DEFAULT_LANG': "en", 'TRANSLATIONS': """{ DEFAULT_LANG: "", @@ -64,6 +67,8 @@ SAMPLE_CONF = { 'TIMEZONE': 'UTC', 'COMMENT_SYSTEM': 'disqus', 'COMMENT_SYSTEM_ID': 'nikolademo', + 'CATEGORY_ALLOW_HIERARCHIES': False, + 'CATEGORY_OUTPUT_FLAT_HIERARCHY': False, 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, 'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK, 'RSS_READ_MORE_LINK': DEFAULT_RSS_READ_MORE_LINK, @@ -103,6 +108,7 @@ SAMPLE_CONF = { 'REDIRECTIONS': [], } + # Generate a list of supported languages here. # Ugly code follows. _suplang = {} @@ -154,8 +160,7 @@ SAMPLE_CONF['_SUPPORTED_COMMENT_SYSTEMS'] = '\n'.join(textwrap.wrap( 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.""" + """Adapt TRANSLATIONS setting for all additional languages.""" if not additional_languages: return SAMPLE_CONF["TRANSLATIONS"] lang_paths = [' DEFAULT_LANG: "",'] @@ -164,12 +169,12 @@ def format_default_translations_config(additional_languages): return "{{\n{0}\n}}".format("\n".join(lang_paths)) -def format_navigation_links(additional_languages, default_lang, messages): +def format_navigation_links(additional_languages, default_lang, messages, strip_indexes=False): """Return the string to configure NAVIGATION_LINKS.""" f = u"""\ {0}: ( ("{1}/archive.html", "{2[Archive]}"), - ("{1}/categories/index.html", "{2[Tags]}"), + ("{1}/categories/{3}", "{2[Tags]}"), ("{1}/rss.xml", "{2[RSS feed]}"), ),""" @@ -185,27 +190,32 @@ def format_navigation_links(additional_languages, default_lang, messages): fmsg[i] = i return fmsg + if strip_indexes: + index_html = '' + else: + index_html = 'index.html' + # handle the default language - pairs.append(f.format('DEFAULT_LANG', '', get_msg(default_lang))) + pairs.append(f.format('DEFAULT_LANG', '', get_msg(default_lang), index_html)) for l in additional_languages: - pairs.append(f.format(json.dumps(l, ensure_ascii=False), '/' + l, get_msg(l))) + pairs.append(f.format(json.dumps(l, ensure_ascii=False), '/' + l, get_msg(l), index_html)) 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(). +# In order to ensure proper escaping, all variables but the pre-formatted ones +# are handled by json.dumps(). def prepare_config(config): """Parse sample config with JSON.""" p = config.copy() - p.update(dict((k, json.dumps(v, ensure_ascii=False)) for k, v in p.items() - if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'RSS_READ_MORE_LINK', 'PRETTY_URLS'))) + p.update({k: json.dumps(v, ensure_ascii=False) for k, v in p.items() + 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("'", "\\'") + "'" - # json would make that `true` instead of `True` - p['PRETTY_URLS'] = str(p['PRETTY_URLS']) + # fix booleans and None + p.update({k: str(v) for k, v in config.items() if isinstance(v, bool) or v is None}) return p @@ -239,11 +249,13 @@ class CommandInit(Command): @classmethod def copy_sample_site(cls, target): + """Copy sample site data to target directory.""" src = resource_filename('nikola', os.path.join('data', 'samplesite')) shutil.copytree(src, target) @staticmethod def create_configuration(target): + """Create configuration file.""" template_path = resource_filename('nikola', 'conf.py.in') conf_template = Template(filename=template_path) conf_path = os.path.join(target, 'conf.py') @@ -252,12 +264,14 @@ class CommandInit(Command): @staticmethod def create_configuration_to_string(): + """Return configuration file as a string.""" template_path = resource_filename('nikola', 'conf.py.in') conf_template = Template(filename=template_path) return conf_template.render(**prepare_config(SAMPLE_CONF)) @classmethod def create_empty_site(cls, target): + """Create an empty site with directories only.""" for folder in ('files', 'galleries', 'listings', 'posts', 'stories'): makedirs(os.path.join(target, folder)) @@ -295,7 +309,8 @@ class CommandInit(Command): SAMPLE_CONF['SITE_URL'] = answer def prettyhandler(default, toconf): - SAMPLE_CONF['PRETTY_URLS'] = ask_yesno('Enable pretty URLs (/page/ instead of /page.html) that don’t need web server configuration?', default=True) + SAMPLE_CONF['PRETTY_URLS'] = ask_yesno('Enable pretty URLs (/page/ instead of /page.html) that don\'t need web server configuration?', default=True) + SAMPLE_CONF['STRIP_INDEXES'] = SAMPLE_CONF['PRETTY_URLS'] def lhandler(default, toconf, show_header=True): if show_header: @@ -333,7 +348,7 @@ class CommandInit(Command): # not inherit from anywhere. try: messages = load_messages(['base'], tr, default) - SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages) + SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages, SAMPLE_CONF['STRIP_INDEXES']) 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).") diff --git a/nikola/plugins/command/install_theme.plugin b/nikola/plugins/command/install_theme.plugin index 54a91ff..8434f2e 100644 --- a/nikola/plugins/command/install_theme.plugin +++ b/nikola/plugins/command/install_theme.plugin @@ -1,10 +1,13 @@ [Core] -Name = install_theme -Module = install_theme +name = install_theme +module = install_theme [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Install a theme into the current site. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Install a theme into the current site. + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py index 4937509..f02252e 100644 --- a/nikola/plugins/command/install_theme.py +++ b/nikola/plugins/command/install_theme.py @@ -24,10 +24,12 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Install a theme.""" + from __future__ import print_function import os import io -import json +import time import requests import pygments @@ -41,6 +43,7 @@ LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER) class CommandInstallTheme(Command): + """Install a theme.""" name = "install_theme" @@ -95,8 +98,13 @@ class CommandInstallTheme(Command): if name is None and not listing: LOGGER.error("This command needs either a theme name or the -l option.") return False - data = requests.get(url).text - data = json.loads(data) + try: + data = requests.get(url).json() + except requests.exceptions.SSLError: + LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") + time.sleep(1) + url = url.replace('https', 'http', 1) + data = requests.get(url).json() if listing: print("Themes:") print("-------") @@ -122,11 +130,21 @@ class CommandInstallTheme(Command): LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname)) def do_install(self, name, data): + """Download and install a theme.""" if name in data: utils.makedirs(self.output_dir) - LOGGER.info("Downloading '{0}'".format(data[name])) + url = data[name] + LOGGER.info("Downloading '{0}'".format(url)) + try: + zip_data = requests.get(url).content + except requests.exceptions.SSLError: + LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") + time.sleep(1) + url = url.replace('https', 'http', 1) + zip_data = requests.get(url).content + zip_file = io.BytesIO() - zip_file.write(requests.get(data[name]).content) + zip_file.write(zip_data) LOGGER.info("Extracting '{0}' into themes/".format(name)) utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) diff --git a/nikola/plugins/command/new_page.plugin b/nikola/plugins/command/new_page.plugin index f078dd6..145a419 100644 --- a/nikola/plugins/command/new_page.plugin +++ b/nikola/plugins/command/new_page.plugin @@ -1,9 +1,13 @@ [Core] -Name = new_page -Module = new_page +name = new_page +module = new_page [Documentation] -Author = Roberto Alsina, Chris Warrick -Version = 1.0 -Website = http://getnikola.com -Description = Create a new page. +author = Roberto Alsina, Chris Warrick +version = 1.0 +website = http://getnikola.com +description = Create a new page. + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/new_page.py b/nikola/plugins/command/new_page.py index 39a85bd..811e28b 100644 --- a/nikola/plugins/command/new_page.py +++ b/nikola/plugins/command/new_page.py @@ -24,12 +24,15 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Create a new page.""" + from __future__ import unicode_literals, print_function from nikola.plugin_categories import Command class CommandNewPage(Command): + """Create a new page.""" name = "new_page" diff --git a/nikola/plugins/command/new_post.plugin b/nikola/plugins/command/new_post.plugin index fec4b1d..d88469f 100644 --- a/nikola/plugins/command/new_post.plugin +++ b/nikola/plugins/command/new_post.plugin @@ -1,10 +1,13 @@ [Core] -Name = new_post -Module = new_post +name = new_post +module = new_post [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create a new post. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create a new post. + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py index 5141c7e..f9fe3ff 100644 --- a/nikola/plugins/command/new_post.py +++ b/nikola/plugins/command/new_post.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Create a new post.""" + from __future__ import unicode_literals, print_function import io import datetime @@ -44,107 +46,8 @@ PAGELOGGER = utils.get_logger('new_page', utils.STDERR_HANDLER) LOGGER = POSTLOGGER -def filter_post_pages(compiler, is_post, compilers, post_pages, compiler_objs, compilers_raw): - """Given a compiler ("markdown", "rest"), and whether it's meant for - a post or a page, and compilers, return the correct entry from - post_pages.""" - - # First throw away all the post_pages with the wrong is_post - filtered = [entry for entry in post_pages if entry[3] == is_post] - - # These are the extensions supported by the required format - extensions = compilers.get(compiler) - if extensions is None: - if compiler in compiler_objs: - LOGGER.error("There is a {0} compiler available, but it's not set in your COMPILERS option.".format(compiler)) - LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) - else: - LOGGER.error('Unknown format {0}'.format(compiler)) - print_compilers(compilers_raw, post_pages, compiler_objs) - return False - - # Throw away the post_pages with the wrong extensions - filtered = [entry for entry in filtered if any([ext in entry[0] for ext in - extensions])] - - if not filtered: - type_name = "post" if is_post else "page" - LOGGER.error("Can't find a way, using your configuration, to create " - "a {0} in format {1}. You may want to tweak " - "COMPILERS or {2}S in conf.py".format( - type_name, compiler, type_name.upper())) - LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) - - return False - return filtered[0] - - -def print_compilers(compilers_raw, post_pages, compiler_objs): - """ - List all available compilers in a human-friendly format. - - :param compilers_raw: The compilers dict, mapping compiler names to tuples of extensions - :param post_pages: The post_pages structure - :param compilers_objs: Compiler objects - """ - - # We use compilers_raw, because the normal dict can contain - # garbage coming from the translation candidate implementation. - # Entries are in format: (name, extensions, used_in_post_pages) - parsed_compilers = {'used': [], 'unused': [], 'disabled': []} - - for compiler_name, compiler_obj in compiler_objs.items(): - fname = compiler_obj.friendly_name or compiler_name - if compiler_name not in compilers_raw: - parsed_compilers['disabled'].append((compiler_name, fname, (), False)) - else: - # stolen from filter_post_pages - extensions = compilers_raw[compiler_name] - filtered = [entry for entry in post_pages if any( - [ext in entry[0] for ext in extensions])] - if filtered: - parsed_compilers['used'].append((compiler_name, fname, extensions, True)) - else: - parsed_compilers['unused'].append((compiler_name, fname, extensions, False)) - - # Sort compilers alphabetically by name, just so it’s prettier (and - # deterministic) - parsed_compilers['used'].sort(key=operator.itemgetter(0)) - parsed_compilers['unused'].sort(key=operator.itemgetter(0)) - parsed_compilers['disabled'].sort(key=operator.itemgetter(0)) - - # We also group the compilers by status for readability. - parsed_list = parsed_compilers['used'] + parsed_compilers['unused'] + parsed_compilers['disabled'] - - print("Available input formats:\n") - - name_width = max([len(i[0]) for i in parsed_list] + [4]) # 4 == len('NAME') - fname_width = max([len(i[1]) for i in parsed_list] + [11]) # 11 == len('DESCRIPTION') - - print((' {0:<' + str(name_width) + '} {1:<' + str(fname_width) + '} EXTENSIONS\n').format('NAME', 'DESCRIPTION')) - - for name, fname, extensions, used in parsed_list: - flag = ' ' if used else '!' - flag = flag if extensions else '~' - - extensions = ', '.join(extensions) if extensions else '(disabled: not in COMPILERS)' - - print(('{flag}{name:<' + str(name_width) + '} {fname:<' + str(fname_width) + '} {extensions}').format(flag=flag, name=name, fname=fname, extensions=extensions)) - - print(""" -More compilers are available in the Plugins Index. - -Compilers marked with ! and ~ require additional configuration: - ! not in the PAGES/POSTS tuples (unused) - ~ not in the COMPILERS dict (disabled) -Read more: {0}""".format(COMPILERS_DOC_LINK)) - - def get_default_compiler(is_post, compilers, post_pages): - """Given compilers and post_pages, return a reasonable - default compiler for this kind of post/page. - """ - + """Given compilers and post_pages, return a reasonable default compiler for this kind of post/page.""" # First throw away all the post_pages with the wrong is_post filtered = [entry for entry in post_pages if entry[3] == is_post] @@ -159,7 +62,7 @@ def get_default_compiler(is_post, compilers, post_pages): def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): - """Returns a date stamp, given a recurrence rule. + """Return a date stamp, given a recurrence rule. schedule - bool: whether to use the recurrence rule or not @@ -177,7 +80,6 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): whether to force ISO 8601 dates (instead of locale-specific ones) """ - if tz is None: tz = dateutil.tz.tzlocal() date = now = datetime.datetime.now(tz) @@ -212,6 +114,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): class CommandNewPost(Command): + """Create a new post.""" name = "new_post" @@ -333,7 +236,7 @@ class CommandNewPost(Command): wants_available = options['available-formats'] if wants_available: - print_compilers(self.site.config['_COMPILERS_RAW'], self.site.config['post_pages'], self.site.compilers) + self.print_compilers() return if is_page: @@ -360,17 +263,13 @@ class CommandNewPost(Command): if content_format not in compiler_names: LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin?".format(content_type, content_format)) - print_compilers(self.site.config['_COMPILERS_RAW'], self.site.config['post_pages'], self.site.compilers) + self.print_compilers() return compiler_plugin = self.site.plugin_manager.getPluginByName( content_format, "PageCompiler").plugin_object # Guess where we should put this - entry = filter_post_pages(content_format, is_post, - self.site.config['COMPILERS'], - self.site.config['post_pages'], - self.site.compilers, - self.site.config['_COMPILERS_RAW']) + entry = self.filter_post_pages(content_format, is_post) if entry is False: return 1 @@ -497,3 +396,122 @@ class CommandNewPost(Command): subprocess.call(to_run) else: LOGGER.error('$EDITOR not set, cannot edit the post. Please do it manually.') + + def filter_post_pages(self, compiler, is_post): + """Return the correct entry from post_pages. + + Information based on: + * selected compilers + * available compilers + * post/page status + """ + compilers = self.site.config['COMPILERS'] + post_pages = self.site.config['post_pages'] + compiler_objs = self.site.compilers + + # First throw away all the post_pages with the wrong is_post + filtered = [entry for entry in post_pages if entry[3] == is_post] + + # These are the extensions supported by the required format + extensions = compilers.get(compiler) + if extensions is None: + if compiler in compiler_objs: + LOGGER.error("There is a {0} compiler available, but it's not set in your COMPILERS option.".format(compiler)) + LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) + else: + LOGGER.error('Unknown format {0}'.format(compiler)) + self.print_compilers() + return False + + # Throw away the post_pages with the wrong extensions + filtered = [entry for entry in filtered if any([ext in entry[0] for ext in + extensions])] + + if not filtered: + type_name = "post" if is_post else "page" + LOGGER.error("Can't find a way, using your configuration, to create " + "a {0} in format {1}. You may want to tweak " + "COMPILERS or {2}S in conf.py".format( + type_name, compiler, type_name.upper())) + LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) + + return False + return filtered[0] + + def print_compilers(self): + """List all available compilers in a human-friendly format.""" + # We use compilers_raw, because the normal dict can contain + # garbage coming from the translation candidate implementation. + # Entries are in format: (name, extensions, used_in_post_pages) + + compilers_raw = self.site.config['_COMPILERS_RAW'] + + used_compilers = [] + unused_compilers = [] + disabled_compilers = [] + + for name, plugin in self.site.compilers.items(): + if name in compilers_raw: + used_compilers.append([ + name, + plugin.friendly_name or name, + compilers_raw[name], + True + ]) + else: + disabled_compilers.append([ + name, + plugin.friendly_name or name, + (), + False + ]) + + for name, (_, _, pi) in self.site.disabled_compilers.items(): + if pi.details.has_option('Nikola', 'Friendlyname'): + f_name = pi.details.get('Nikola', 'Friendlyname') + else: + f_name = name + if name in compilers_raw: + unused_compilers.append([ + name, + f_name, + compilers_raw[name], + False + ]) + else: + disabled_compilers.append([ + name, + f_name, + (), + False + ]) + + used_compilers.sort(key=operator.itemgetter(0)) + unused_compilers.sort(key=operator.itemgetter(0)) + disabled_compilers.sort(key=operator.itemgetter(0)) + + # We also group the compilers by status for readability. + parsed_list = used_compilers + unused_compilers + disabled_compilers + + print("Available input formats:\n") + + name_width = max([len(i[0]) for i in parsed_list] + [4]) # 4 == len('NAME') + fname_width = max([len(i[1]) for i in parsed_list] + [11]) # 11 == len('DESCRIPTION') + + print((' {0:<' + str(name_width) + '} {1:<' + str(fname_width) + '} EXTENSIONS\n').format('NAME', 'DESCRIPTION')) + + for name, fname, extensions, used in parsed_list: + flag = ' ' if used else '!' + flag = flag if extensions else '~' + + extensions = ', '.join(extensions) if extensions else '(disabled: not in COMPILERS)' + + print(('{flag}{name:<' + str(name_width) + '} {fname:<' + str(fname_width) + '} {extensions}').format(flag=flag, name=name, fname=fname, extensions=extensions)) + + print(""" + More compilers are available in the Plugins Index. + + Compilers marked with ! and ~ require additional configuration: + ! not in the PAGES/POSTS tuples (unused) + ~ not in the COMPILERS dict (disabled) + Read more: {0}""".format(COMPILERS_DOC_LINK)) diff --git a/nikola/plugins/command/orphans.plugin b/nikola/plugins/command/orphans.plugin index f491eaf..669429d 100644 --- a/nikola/plugins/command/orphans.plugin +++ b/nikola/plugins/command/orphans.plugin @@ -1,10 +1,13 @@ [Core] -Name = orphans -Module = orphans +name = orphans +module = orphans [Documentation] -Author = Roberto Alsina, Chris Warrick -Version = 1.0 -Website = http://getnikola.com -Description = List all orphans +author = Roberto Alsina, Chris Warrick +version = 1.0 +website = http://getnikola.com +description = List all orphans + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/orphans.py b/nikola/plugins/command/orphans.py index f550e17..b12cc67 100644 --- a/nikola/plugins/command/orphans.py +++ b/nikola/plugins/command/orphans.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""List all orphans.""" + from __future__ import print_function import os @@ -32,6 +34,9 @@ from nikola.plugins.command.check import real_scan_files class CommandOrphans(Command): + + """List all orphans.""" + name = "orphans" doc_purpose = "list all orphans" doc_description = """\ @@ -41,5 +46,6 @@ but are not generated by Nikola. Output contains filenames only (it is passable to `xargs rm` or the like).""" def _execute(self, options, args): + """Run the orphans command.""" orphans = real_scan_files(self.site)[0] print('\n'.join([p for p in orphans if not os.path.isdir(p)])) diff --git a/nikola/plugins/command/plugin.plugin b/nikola/plugins/command/plugin.plugin index 2815caa..d44dcf3 100644 --- a/nikola/plugins/command/plugin.plugin +++ b/nikola/plugins/command/plugin.plugin @@ -1,10 +1,13 @@ [Core] -Name = plugin -Module = plugin +name = plugin +module = plugin [Documentation] -Author = Roberto Alsina and Chris Warrick -Version = 1.0 -Website = http://getnikola.com -Description = Manage Nikola plugins +author = Roberto Alsina and Chris Warrick +version = 1.0 +website = http://getnikola.com +description = Manage Nikola plugins + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py index 56eb1d7..f892ee9 100644 --- a/nikola/plugins/command/plugin.py +++ b/nikola/plugins/command/plugin.py @@ -24,12 +24,14 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Manage plugins.""" + from __future__ import print_function import io import os import shutil import subprocess -import sys +import time import requests import pygments @@ -43,6 +45,7 @@ LOGGER = utils.get_logger('plugin', utils.STDERR_HANDLER) class CommandPlugin(Command): + """Manage plugins.""" json = None @@ -119,6 +122,7 @@ class CommandPlugin(Command): upgrade = options.get('upgrade') list_available = options.get('list') list_installed = options.get('list_installed') + show_install_notes = options.get('show_install_notes', True) command_count = [bool(x) for x in ( install, uninstall, @@ -127,37 +131,42 @@ class CommandPlugin(Command): list_installed)].count(True) if command_count > 1 or command_count == 0: print(self.help()) - return + return 2 - 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') + if options.get('output_dir') is not None: + self.output_dir = options.get('output_dir') else: - self.output_dir = 'plugins' + 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) + return self.list_available(url) elif list_installed: - self.list_installed() + return self.list_installed() elif upgrade: - self.do_upgrade(url) + return self.do_upgrade(url) elif uninstall: - self.do_uninstall(uninstall) + return self.do_uninstall(uninstall) elif install: - self.do_install(url, install) + return self.do_install(url, install, show_install_notes) def list_available(self, url): + """List all available plugins.""" data = self.get_json(url) print("Available Plugins:") print("------------------") for plugin in sorted(data.keys()): print(plugin) - return True + return 0 def list_installed(self): + """List installed plugins.""" plugins = [] for plugin in self.site.plugin_manager.getAllPlugins(): p = plugin.path @@ -170,8 +179,10 @@ class CommandPlugin(Command): plugins.sort() for name, path in plugins: print('{0} at {1}'.format(name, path)) + return 0 def do_upgrade(self, url): + """Upgrade all installed plugins.""" LOGGER.warning('This is not very smart, it just reinstalls some plugins and hopes for the best') data = self.get_json(url) plugins = [] @@ -194,18 +205,29 @@ class CommandPlugin(Command): break elif tail == '': LOGGER.error("Can't find the plugins folder for path: {0}".format(p)) - return False + return 1 else: path = tail self.do_install(url, name) + return 0 - def do_install(self, url, name): + def do_install(self, url, name, show_install_notes=True): + """Download and install a plugin.""" data = self.get_json(url) if name in data: utils.makedirs(self.output_dir) - LOGGER.info('Downloading: ' + data[name]) + url = data[name] + LOGGER.info("Downloading '{0}'".format(url)) + try: + zip_data = requests.get(url).content + except requests.exceptions.SSLError: + LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") + time.sleep(1) + url = url.replace('https', 'http', 1) + zip_data = requests.get(url).content + zip_file = io.BytesIO() - zip_file.write(requests.get(data[name]).content) + zip_file.write(zip_data) 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) @@ -214,13 +236,13 @@ class CommandPlugin(Command): plugin_path = utils.get_plugin_path(name) except: LOGGER.error("Can't find plugin " + name) - return False + return 1 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 + return 1 LOGGER.info('Copying {0} into plugins'.format(plugin_path)) shutil.copytree(plugin_path, dest_path) @@ -256,7 +278,7 @@ class CommandPlugin(Command): 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): + if os.path.exists(confpypath) and show_install_notes: 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 io.open(confpypath, 'r', encoding='utf-8') as fh: @@ -266,9 +288,10 @@ class CommandPlugin(Command): 4 * ' ')) else: print(utils.indent(fh.read(), 4 * ' ')) - return True + return 0 def do_uninstall(self, name): + """Uninstall a plugin.""" for plugin in self.site.plugin_manager.getAllPlugins(): # FIXME: this is repeated thrice p = plugin.path if os.path.isdir(p): @@ -278,16 +301,23 @@ class CommandPlugin(Command): 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'): + sure = utils.ask_yesno('Are you sure?') + if sure: LOGGER.warning('Removing {0}'.format(p)) shutil.rmtree(p) - return True + return 0 + return 1 LOGGER.error('Unknown plugin: {0}'.format(name)) - return False + return 1 def get_json(self, url): + """Download the JSON file with all plugins.""" if self.json is None: - self.json = requests.get(url).json() + try: + self.json = requests.get(url).json() + except requests.exceptions.SSLError: + LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") + time.sleep(1) + url = url.replace('https', 'http', 1) + self.json = requests.get(url).json() return self.json diff --git a/nikola/plugins/command/rst2html.plugin b/nikola/plugins/command/rst2html.plugin index 0d0d3b0..02c9276 100644 --- a/nikola/plugins/command/rst2html.plugin +++ b/nikola/plugins/command/rst2html.plugin @@ -1,9 +1,13 @@ [Core] -Name = rst2html -Module = rst2html +name = rst2html +module = rst2html [Documentation] -Author = Chris Warrick -Version = 1.0 -Website = http://getnikola.com -Description = Compile reStructuredText to HTML using the Nikola architecture +author = Chris Warrick +version = 1.0 +website = http://getnikola.com +description = Compile reStructuredText to HTML using the Nikola architecture + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/rst2html/__init__.py b/nikola/plugins/command/rst2html/__init__.py index 342aaeb..06afffd 100644 --- a/nikola/plugins/command/rst2html/__init__.py +++ b/nikola/plugins/command/rst2html/__init__.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Compile reStructuredText to HTML, using Nikola architecture.""" + from __future__ import unicode_literals, print_function import io @@ -34,6 +36,7 @@ from nikola.plugin_categories import Command class CommandRst2Html(Command): + """Compile reStructuredText to HTML, using Nikola architecture.""" name = "rst2html" diff --git a/nikola/plugins/command/serve.plugin b/nikola/plugins/command/serve.plugin index 0c1176d..aca71ec 100644 --- a/nikola/plugins/command/serve.plugin +++ b/nikola/plugins/command/serve.plugin @@ -1,10 +1,13 @@ [Core] -Name = serve -Module = serve +name = serve +module = serve [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Start test server. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Start test server. + +[Nikola] +plugincategory = Command diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index 0e4d01f..0441c93 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -24,8 +24,11 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Start test server.""" + from __future__ import print_function import os +import re import socket import webbrowser try: @@ -35,16 +38,25 @@ except ImportError: from http.server import HTTPServer # NOQA from http.server import SimpleHTTPRequestHandler # NOQA +try: + from StringIO import StringIO +except ImportError: + from io import BytesIO as StringIO # NOQA + + from nikola.plugin_categories import Command -from nikola.utils import get_logger +from nikola.utils import get_logger, STDERR_HANDLER class IPv6Server(HTTPServer): + """An IPv6 HTTPServer.""" + address_family = socket.AF_INET6 class CommandServe(Command): + """Start test server.""" name = "serve" @@ -70,6 +82,14 @@ class CommandServe(Command): 'help': 'Address to bind (default: 0.0.0.0 – all local IPv4 interfaces)', }, { + 'name': 'detach', + 'short': 'd', + 'long': 'detach', + 'type': bool, + 'default': False, + 'help': 'Detach from TTY (work in the background)', + }, + { 'name': 'browser', 'short': 'b', 'long': 'browser', @@ -89,7 +109,7 @@ class CommandServe(Command): def _execute(self, options, args): """Start test server.""" - self.logger = get_logger('serve', self.site.loghandlers) + self.logger = get_logger('serve', STDERR_HANDLER) out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): self.logger.error("Missing '{0}' folder?".format(out_dir)) @@ -117,16 +137,42 @@ class CommandServe(Command): server_url = "http://{0}:{1}/".format(*sa) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open(server_url) - try: - httpd.serve_forever() - except KeyboardInterrupt: - self.logger.info("Server is shutting down.") - return 130 + if options['detach']: + OurHTTPRequestHandler.quiet = True + try: + pid = os.fork() + if pid == 0: + httpd.serve_forever() + else: + self.logger.info("Detached with PID {0}. Run `kill {0}` to stop the server.".format(pid)) + except AttributeError as e: + if os.name == 'nt': + self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") + else: + raise e + else: + try: + httpd.serve_forever() + except KeyboardInterrupt: + self.logger.info("Server is shutting down.") + return 130 class OurHTTPRequestHandler(SimpleHTTPRequestHandler): + + """A request handler, modified for Nikola.""" + extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) extensions_map[""] = "text/plain" + quiet = False + + def log_message(self, *args): + """Log messages. Or not, depending on a setting.""" + if self.quiet: + return + else: + # Old-style class in Python 2.7, cannot use super() + return SimpleHTTPRequestHandler.log_message(self, *args) # NOTICE: this is a patched version of send_head() to disable all sorts of # caching. `nikola serve` is a development server, hence caching should @@ -182,14 +228,31 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): except IOError: self.send_error(404, "File not found") return None + + filtered_bytes = None + if ctype == 'text/html': + # Comment out any <base> to allow local resolution of relative URLs. + data = f.read().decode('utf8') + f.close() + data = re.sub(r'<base\s([^>]*)>', '<!--base \g<1>-->', data, re.IGNORECASE) + data = data.encode('utf8') + f = StringIO() + f.write(data) + filtered_bytes = len(data) + f.seek(0) + self.send_response(200) self.send_header("Content-type", ctype) if os.path.splitext(path)[1] == '.svgz': # Special handling for svgz to make it work nice with browsers. self.send_header("Content-Encoding", 'gzip') - fs = os.fstat(f.fileno()) - self.send_header("Content-Length", str(fs[6])) - self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + + if filtered_bytes is None: + fs = os.fstat(f.fileno()) + self.send_header('Content-Length', str(fs[6])) + else: + self.send_header('Content-Length', filtered_bytes) + # begin no-cache patch # For standard requests. self.send_header("Cache-Control", "no-cache, no-store, " diff --git a/nikola/plugins/command/status.plugin b/nikola/plugins/command/status.plugin index e02da8b..91390d2 100644 --- a/nikola/plugins/command/status.plugin +++ b/nikola/plugins/command/status.plugin @@ -1,9 +1,13 @@ [Core] -Name = status -Module = status +name = status +module = status [Documentation] -Author = Daniel Aleksandersen -Version = 1.0 -Website = https://getnikola.com -Description = Site status +author = Daniel Aleksandersen +version = 1.0 +website = https://getnikola.com +description = Site status + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/status.py b/nikola/plugins/command/status.py index b8a6a60..55e7f95 100644 --- a/nikola/plugins/command/status.py +++ b/nikola/plugins/command/status.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Display site status.""" + from __future__ import print_function import io import os @@ -33,8 +35,10 @@ from dateutil.tz import gettz, tzlocal from nikola.plugin_categories import Command -class CommandDeploy(Command): - """ Site status. """ +class CommandStatus(Command): + + """Display site status.""" + name = "status" doc_purpose = "display site status" @@ -69,7 +73,7 @@ class CommandDeploy(Command): ] def _execute(self, options, args): - + """Display site status.""" self.site.scan_posts() timestamp_path = os.path.join(self.site.config["CACHE_FOLDER"], "lastdeploy") @@ -128,6 +132,7 @@ class CommandDeploy(Command): print("{0} posts in total, {1} scheduled, and {2} drafts.".format(posts_count, len(posts_scheduled), len(posts_drafts))) def human_time(self, dt): + """Translate time into a human-friendly representation.""" days = dt.days hours = dt.seconds / 60 // 60 minutes = dt.seconds / 60 - (hours * 60) diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin index a3f58e8..4708bdb 100644 --- a/nikola/plugins/command/version.plugin +++ b/nikola/plugins/command/version.plugin @@ -1,9 +1,13 @@ [Core] -Name = version -Module = version +name = version +module = version [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Show nikola version +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Show nikola version + +[Nikola] +plugincategory = Command + diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py index b6520d7..ad08f64 100644 --- a/nikola/plugins/command/version.py +++ b/nikola/plugins/command/version.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Print Nikola version.""" + from __future__ import print_function import lxml @@ -36,7 +38,8 @@ URL = 'https://pypi.python.org/pypi?:action=doap&name=Nikola' class CommandVersion(Command): - """Print the version.""" + + """Print Nikola version.""" name = "version" diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py index a1d17a6..60f1919 100644 --- a/nikola/plugins/compile/__init__.py +++ b/nikola/plugins/compile/__init__.py @@ -23,3 +23,5 @@ # 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. + +"""Compilers for Nikola.""" diff --git a/nikola/plugins/compile/html.plugin b/nikola/plugins/compile/html.plugin index 66623b2..53ade61 100644 --- a/nikola/plugins/compile/html.plugin +++ b/nikola/plugins/compile/html.plugin @@ -1,10 +1,13 @@ [Core] -Name = html -Module = html +name = html +module = html [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Compile HTML into HTML (just copy) +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Compile HTML into HTML (just copy) +[Nikola] +plugincategory = Compiler +friendlyname = HTML diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py index ab0c2f6..5f8b244 100644 --- a/nikola/plugins/compile/html.py +++ b/nikola/plugins/compile/html.py @@ -36,11 +36,14 @@ from nikola.utils import makedirs, write_metadata class CompileHtml(PageCompiler): + """Compile HTML into HTML.""" + name = "html" friendly_name = "HTML" def compile_html(self, source, dest, is_two_file=True): + """Compile source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) with io.open(dest, "w+", encoding="utf8") as out_file: with io.open(source, "r", encoding="utf8") as in_file: @@ -51,6 +54,7 @@ class CompileHtml(PageCompiler): return True def create_post(self, path, **kw): + """Create a new post.""" content = kw.pop('content', None) onefile = kw.pop('onefile', False) # is_page is not used by create_post as of now. diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin index efe6702..c369ab2 100644 --- a/nikola/plugins/compile/ipynb.plugin +++ b/nikola/plugins/compile/ipynb.plugin @@ -1,10 +1,13 @@ [Core] -Name = ipynb -Module = ipynb +name = ipynb +module = ipynb [Documentation] -Author = Damian Avila, Chris Warrick and others -Version = 2.0.0 -Website = http://www.damian.oquanta.info/ -Description = Compile IPython notebooks into Nikola posts +author = Damian Avila, Chris Warrick and others +version = 2.0.0 +website = http://www.damian.oquanta.info/ +description = Compile IPython notebooks into Nikola posts +[Nikola] +plugincategory = Compiler +friendlyname = Jupyter/IPython Notebook diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py index 82b76c8..a9dedde 100644 --- a/nikola/plugins/compile/ipynb.py +++ b/nikola/plugins/compile/ipynb.py @@ -49,10 +49,11 @@ except ImportError: flag = None from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing, get_logger +from nikola.utils import makedirs, req_missing, get_logger, STDERR_HANDLER class CompileIPynb(PageCompiler): + """Compile IPynb into HTML.""" name = "ipynb" @@ -61,24 +62,30 @@ class CompileIPynb(PageCompiler): default_kernel = 'python2' if sys.version_info[0] == 2 else 'python3' def set_site(self, site): - self.logger = get_logger('compile_ipynb', site.loghandlers) + """Set Nikola site.""" + self.logger = get_logger('compile_ipynb', STDERR_HANDLER) super(CompileIPynb, self).set_site(site) - def compile_html(self, source, dest, is_two_file=True): + def compile_html_string(self, source, is_two_file=True): + """Export notebooks as HTML strings.""" if flag is None: req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') - makedirs(os.path.dirname(dest)) HTMLExporter.default_template = 'basic' c = Config(self.site.config['IPYNB_CONFIG']) exportHtml = HTMLExporter(config=c) + with io.open(source, "r", encoding="utf8") as in_file: + nb_json = nbformat.read(in_file, current_nbformat) + (body, resources) = exportHtml.from_notebook_node(nb_json) + return body + + def compile_html(self, source, dest, is_two_file=True): + """Compile source file into HTML and save as dest.""" + makedirs(os.path.dirname(dest)) with io.open(dest, "w+", encoding="utf8") as out_file: - with io.open(source, "r", encoding="utf8") as in_file: - nb_json = nbformat.read(in_file, current_nbformat) - (body, resources) = exportHtml.from_notebook_node(nb_json) - out_file.write(body) + out_file.write(self.compile_html_string(source, is_two_file)) def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): - """read metadata directly from ipynb file. + """Read metadata directly from ipynb file. As ipynb file support arbitrary metadata as json, the metadata used by Nikola will be assume to be in the 'nikola' subfield. @@ -93,6 +100,7 @@ class CompileIPynb(PageCompiler): return nb_json.get('metadata', {}).get('nikola', {}) def create_post(self, path, **kw): + """Create a new post.""" if flag is None: req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') content = kw.pop('content', None) diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin index a44b798..f7d11b1 100644 --- a/nikola/plugins/compile/markdown.plugin +++ b/nikola/plugins/compile/markdown.plugin @@ -1,10 +1,13 @@ [Core] -Name = markdown -Module = markdown +name = markdown +module = markdown [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Compile Markdown into HTML +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Compile Markdown into HTML +[Nikola] +plugincategory = Compiler +friendlyname = Markdown diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index fbe049d..c1425a1 100644 --- a/nikola/plugins/compile/markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -44,6 +44,7 @@ from nikola.utils import makedirs, req_missing, write_metadata class CompileMarkdown(PageCompiler): + """Compile Markdown into HTML.""" name = "markdown" @@ -53,21 +54,18 @@ class CompileMarkdown(PageCompiler): site = None def set_site(self, site): + """Set Nikola site.""" + super(CompileMarkdown, self).set_site(site) self.config_dependencies = [] - 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 + for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) - 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 self.config_dependencies.append(str(sorted(site.config.get("MARKDOWN_EXTENSIONS")))) - return super(CompileMarkdown, self).set_site(site) def compile_html(self, source, dest, is_two_file=True): + """Compile source file into HTML and save as dest.""" if markdown is None: req_missing(['markdown'], 'build this site (compile Markdown)') makedirs(os.path.dirname(dest)) @@ -81,6 +79,7 @@ class CompileMarkdown(PageCompiler): out_file.write(output) def create_post(self, path, **kw): + """Create a new post.""" content = kw.pop('content', None) onefile = kw.pop('onefile', False) # is_page is not used by create_post as of now. diff --git a/nikola/plugins/compile/markdown/mdx_gist.plugin b/nikola/plugins/compile/markdown/mdx_gist.plugin index 0e5c578..7fe676c 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.plugin +++ b/nikola/plugins/compile/markdown/mdx_gist.plugin @@ -1,9 +1,14 @@ [Core] -Name = mdx_gist -Module = mdx_gist +name = mdx_gist +module = mdx_gist + +[Nikola] +compiler = markdown +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Extension for embedding gists +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Extension for embedding gists + diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index 70e7394..f439fa2 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -26,16 +26,16 @@ # # Inspired by "[Python] reStructuredText GitHub Gist directive" # (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu -''' -Extension to Python Markdown for Embedded Gists (gist.github.com) +""" +Extension to Python Markdown for Embedded Gists (gist.github.com). Basic Example: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: 4747847] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -50,10 +50,10 @@ Basic Example: Example with filename: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: 4747847 zen.py] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -68,10 +68,10 @@ Example with filename: Basic Example with hexidecimal id: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: c4a43d6fdce612284ac0] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -86,10 +86,10 @@ Basic Example with hexidecimal id: Example with hexidecimal id filename: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: c4a43d6fdce612284ac0 cow.txt] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -104,10 +104,10 @@ Example with hexidecimal id filename: Example using reStructuredText syntax: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... .. gist:: 4747847 zen.py - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -122,10 +122,10 @@ Example using reStructuredText syntax: Example using hexidecimal ID with reStructuredText syntax: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... .. gist:: c4a43d6fdce612284ac0 - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -140,10 +140,10 @@ Example using hexidecimal ID with reStructuredText syntax: Example using hexidecimal ID and filename with reStructuredText syntax: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... .. gist:: c4a43d6fdce612284ac0 cow.txt - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: @@ -158,38 +158,36 @@ Example using hexidecimal ID and filename with reStructuredText syntax: Error Case: non-existent Gist ID: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: 0] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: <div class="gist"> <script src="https://gist.github.com/0.js"></script> - <noscript><!-- WARNING: Received a 404 response from Gist URL: \ -https://gist.githubusercontent.com/raw/0 --></noscript> + <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.githubusercontent.com/raw/0 --></noscript> </div> </p> Error Case: non-existent file: >>> import markdown - >>> text = """ + >>> text = ''' ... Text of the gist: ... [:gist: 4747847 doesntexist.py] - ... """ + ... ''' >>> html = markdown.markdown(text, [GistExtension()]) >>> print(html) <p>Text of the gist: <div class="gist"> <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script> - <noscript><!-- WARNING: Received a 404 response from Gist URL: \ -https://gist.githubusercontent.com/raw/4747847/doesntexist.py --></noscript> + <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.githubusercontent.com/raw/4747847/doesntexist.py --></noscript> </div> </p> +""" -''' from __future__ import unicode_literals, print_function try: @@ -219,20 +217,26 @@ GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>[^\]\s]+)(?:\s*(?P<filename>.+? class GistFetchException(Exception): - '''Raised when attempt to fetch content of a Gist from github.com fails.''' + + """Raised when attempt to fetch content of a Gist from github.com fails.""" + def __init__(self, url, status_code): + """Initialize the exception.""" Exception.__init__(self) self.message = 'Received a {0} response from Gist URL: {1}'.format( status_code, url) class GistPattern(Pattern): - """ InlinePattern for footnote markers in a document's body text. """ + + """InlinePattern for footnote markers in a document's body text.""" def __init__(self, pattern, configs): + """Initialize the pattern.""" Pattern.__init__(self, pattern) def get_raw_gist_with_filename(self, gist_id, filename): + """Get raw gist text for a filename.""" url = GIST_FILE_RAW_URL.format(gist_id, filename) resp = requests.get(url) @@ -242,6 +246,7 @@ class GistPattern(Pattern): return resp.text def get_raw_gist(self, gist_id): + """Get raw gist text.""" url = GIST_RAW_URL.format(gist_id) resp = requests.get(url) @@ -251,6 +256,7 @@ class GistPattern(Pattern): return resp.text def handleMatch(self, m): + """Handle pattern match.""" gist_id = m.group('gist_id') gist_file = m.group('filename') @@ -284,7 +290,11 @@ class GistPattern(Pattern): class GistExtension(MarkdownExtension, Extension): + + """Gist extension for Markdown.""" + def __init__(self, configs={}): + """Initialize the extension.""" # set extension defaults self.config = {} @@ -293,6 +303,7 @@ class GistExtension(MarkdownExtension, Extension): self.setConfig(key, value) def extendMarkdown(self, md, md_globals): + """Extend Markdown.""" gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs()) gist_md_pattern.md = md md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong") @@ -304,7 +315,8 @@ class GistExtension(MarkdownExtension, Extension): md.registerExtension(self) -def makeExtension(configs=None): +def makeExtension(configs=None): # pragma: no cover + """Make Markdown extension.""" return GistExtension(configs) if __name__ == '__main__': diff --git a/nikola/plugins/compile/markdown/mdx_nikola.plugin b/nikola/plugins/compile/markdown/mdx_nikola.plugin index 7af52a4..12e4fb6 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.plugin +++ b/nikola/plugins/compile/markdown/mdx_nikola.plugin @@ -1,9 +1,14 @@ [Core] -Name = mdx_nikola -Module = mdx_nikola +name = mdx_nikola +module = mdx_nikola + +[Nikola] +compiler = markdown +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Nikola-specific Markdown extensions +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Nikola-specific Markdown extensions + diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py index a03547f..54cc18c 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.py +++ b/nikola/plugins/compile/markdown/mdx_nikola.py @@ -24,7 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Markdown Extension for Nikola-specific post-processing""" +"""Markdown Extension for Nikola-specific post-processing.""" + from __future__ import unicode_literals import re try: @@ -41,7 +42,11 @@ CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.M class NikolaPostProcessor(Postprocessor): + + """Nikola-specific post-processing for Markdown.""" + def run(self, text): + """Run the postprocessor.""" output = text # python-markdown's highlighter uses <div class="codehilite"><pre> @@ -52,11 +57,16 @@ class NikolaPostProcessor(Postprocessor): class NikolaExtension(MarkdownExtension, Extension): + + """Extension for injecting the postprocessor.""" + def extendMarkdown(self, md, md_globals): + """Extend Markdown with the postprocessor.""" pp = NikolaPostProcessor() md.postprocessors.add('nikola_post_processor', pp, '_end') md.registerExtension(self) -def makeExtension(configs=None): +def makeExtension(configs=None): # pragma: no cover + """Make extension.""" return NikolaExtension(configs) diff --git a/nikola/plugins/compile/markdown/mdx_podcast.plugin b/nikola/plugins/compile/markdown/mdx_podcast.plugin index dc16044..c92a8a0 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.plugin +++ b/nikola/plugins/compile/markdown/mdx_podcast.plugin @@ -1,9 +1,14 @@ [Core] -Name = mdx_podcast -Module = mdx_podcast +name = mdx_podcast +module = mdx_podcast + +[Nikola] +compiler = markdown +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Markdown extensions for embedding podcasts and other audio files +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 670973a..61afdbf 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.py +++ b/nikola/plugins/compile/markdown/mdx_podcast.py @@ -24,21 +24,19 @@ # Inspired by "[Python] reStructuredText GitHub Podcast directive" # (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu -from __future__ import print_function, unicode_literals - - -''' -Extension to Python Markdown for Embedded Audio +""" +Extension to Python Markdown for Embedded Audio. Basic Example: >>> import markdown ->>> text = """[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]""" +>>> text = "[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]" >>> html = markdown.markdown(text, [PodcastExtension()]) >>> print(html) -<p><audio src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3"></audio></p> -''' +<p><audio controls=""><source src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3" type="audio/mpeg"></source></audio></p> +""" +from __future__ import print_function, unicode_literals from nikola.plugin_categories import MarkdownExtension try: from markdown.extensions import Extension @@ -53,12 +51,15 @@ PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]' class PodcastPattern(Pattern): - """ InlinePattern for footnote markers in a document's body text. """ + + """InlinePattern for footnote markers in a document's body text.""" def __init__(self, pattern, configs): + """Initialize pattern.""" Pattern.__init__(self, pattern) def handleMatch(self, m): + """Handle pattern matches.""" url = m.group('url').strip() audio_elem = etree.Element('audio') audio_elem.set('controls', '') @@ -69,7 +70,11 @@ class PodcastPattern(Pattern): class PodcastExtension(MarkdownExtension, Extension): + + """"Podcast extension for Markdown.""" + def __init__(self, configs={}): + """Initialize extension.""" # set extension defaults self.config = {} @@ -78,13 +83,15 @@ class PodcastExtension(MarkdownExtension, Extension): self.setConfig(key, value) def extendMarkdown(self, md, md_globals): + """Extend Markdown.""" podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs()) podcast_md_pattern.md = md md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong") md.registerExtension(self) -def makeExtension(configs=None): +def makeExtension(configs=None): # pragma: no cover + """Make Markdown extension.""" return PodcastExtension(configs) if __name__ == '__main__': diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin index ad54b3b..3ff3668 100644 --- a/nikola/plugins/compile/pandoc.plugin +++ b/nikola/plugins/compile/pandoc.plugin @@ -1,10 +1,13 @@ [Core] -Name = pandoc -Module = pandoc +name = pandoc +module = pandoc [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Compile markups into HTML using pandoc +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Compile markups into HTML using pandoc +[Nikola] +plugincategory = Compiler +friendlyname = Pandoc diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 361f158..3030626 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -27,7 +27,6 @@ """Implementation of compile_html based on pandoc. You will need, of course, to install pandoc - """ from __future__ import unicode_literals @@ -41,16 +40,19 @@ from nikola.utils import req_missing, makedirs, write_metadata class CompilePandoc(PageCompiler): + """Compile markups into HTML using pandoc.""" name = "pandoc" friendly_name = "pandoc" def set_site(self, site): + """Set Nikola site.""" self.config_dependencies = [str(site.config['PANDOC_OPTIONS'])] super(CompilePandoc, self).set_site(site) def compile_html(self, source, dest, is_two_file=True): + """Compile source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) try: subprocess.check_call(['pandoc', '-o', dest, source] + self.site.config['PANDOC_OPTIONS']) @@ -59,6 +61,7 @@ class CompilePandoc(PageCompiler): req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False) def create_post(self, path, **kw): + """Create a new post.""" content = kw.pop('content', None) onefile = kw.pop('onefile', False) # is_page is not used by create_post as of now. diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin index d6623b5..151c022 100644 --- a/nikola/plugins/compile/php.plugin +++ b/nikola/plugins/compile/php.plugin @@ -1,10 +1,13 @@ [Core] -Name = php -Module = php +name = php +module = php [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Compile PHP into HTML (just copy and name the file .php) +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Compile PHP into HTML (just copy and name the file .php) +[Nikola] +plugincategory = Compiler +friendlyname = PHP diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index bb436e5..28f4923 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -37,12 +37,14 @@ from hashlib import md5 class CompilePhp(PageCompiler): + """Compile PHP into PHP.""" name = "php" friendly_name = "PHP" def compile_html(self, source, dest, is_two_file=True): + """Compile source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) with io.open(dest, "w+", encoding="utf8") as out_file: with open(source, "rb") as in_file: @@ -51,6 +53,7 @@ class CompilePhp(PageCompiler): return True def create_post(self, path, **kw): + """Create a new post.""" content = kw.pop('content', None) onefile = kw.pop('onefile', False) # is_page is not used by create_post as of now. @@ -80,4 +83,5 @@ class CompilePhp(PageCompiler): fd.write(content) def extension(self): + """Return extension used for PHP files.""" return ".php" diff --git a/nikola/plugins/compile/rest.plugin b/nikola/plugins/compile/rest.plugin index f144809..cf842c7 100644 --- a/nikola/plugins/compile/rest.plugin +++ b/nikola/plugins/compile/rest.plugin @@ -1,10 +1,13 @@ [Core] -Name = rest -Module = rest +name = rest +module = rest [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Compile reSt into HTML +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Compile reSt into HTML +[Nikola] +plugincategory = Compiler +friendlyname = reStructuredText diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py index d446fe8..b99e872 100644 --- a/nikola/plugins/compile/rest/__init__.py +++ b/nikola/plugins/compile/rest/__init__.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""reStructuredText compiler for Nikola.""" + from __future__ import unicode_literals import io import os @@ -36,10 +38,11 @@ import docutils.readers.standalone import docutils.writers.html4css1 from nikola.plugin_categories import PageCompiler -from nikola.utils import unicode_str, get_logger, makedirs, write_metadata +from nikola.utils import unicode_str, get_logger, makedirs, write_metadata, STDERR_HANDLER class CompileRest(PageCompiler): + """Compile reStructuredText into HTML.""" name = "rest" @@ -48,7 +51,7 @@ class CompileRest(PageCompiler): logger = None def _read_extra_deps(self, post): - """Reads contents of .dep file and returns them as a list""" + """Read contents of .dep file and returns them as a list.""" dep_path = post.base_path + '.dep' if os.path.isfile(dep_path): with io.open(dep_path, 'r+', encoding='utf8') as depf: @@ -57,11 +60,11 @@ class CompileRest(PageCompiler): return [] def register_extra_dependencies(self, post): - """Adds dependency to post object to check .dep file.""" + """Add dependency to post object to check .dep file.""" post.add_dependency(lambda: self._read_extra_deps(post), 'fragment') def compile_html_string(self, data, source_path=None, is_two_file=True): - """Compile reSt into HTML strings.""" + """Compile reST into HTML strings.""" # If errors occur, this will be added to the line number reported by # docutils so the line number matches the actual line number (off by # 7 with default metadata, could be more or less depending on the post). @@ -88,7 +91,7 @@ class CompileRest(PageCompiler): return output, error_level, deps def compile_html(self, source, dest, is_two_file=True): - """Compile reSt into HTML files.""" + """Compile source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) error_level = 100 with io.open(dest, "w+", encoding="utf8") as out_file: @@ -110,6 +113,7 @@ class CompileRest(PageCompiler): return False def create_post(self, path, **kw): + """Create a new post.""" content = kw.pop('content', None) onefile = kw.pop('onefile', False) # is_page is not used by create_post as of now. @@ -127,23 +131,17 @@ class CompileRest(PageCompiler): fd.write(content) def set_site(self, site): + """Set Nikola site.""" + super(CompileRest, self).set_site(site) self.config_dependencies = [] - for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"): - if plugin_info.name in site.config['DISABLED_PLUGINS']: - site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension") - continue - - site.plugin_manager.activatePluginByName(plugin_info.name) + for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) - plugin_info.plugin_object.set_site(site) plugin_info.plugin_object.short_help = plugin_info.description - self.logger = get_logger('compile_rest', site.loghandlers) + self.logger = get_logger('compile_rest', STDERR_HANDLER) if not site.debug: self.logger.level = 4 - return super(CompileRest, self).set_site(site) - def get_observer(settings): """Return an observer for the docutils Reporter.""" @@ -175,11 +173,15 @@ def get_observer(settings): class NikolaReader(docutils.readers.standalone.Reader): + """Nikola-specific docutils reader.""" + def __init__(self, *args, **kwargs): + """Initialize the reader.""" self.transforms = kwargs.pop('transforms', []) docutils.readers.standalone.Reader.__init__(self, *args, **kwargs) def get_transforms(self): + """Get docutils transforms.""" return docutils.readers.standalone.Reader(self).get_transforms() + self.transforms def new_document(self): @@ -191,8 +193,8 @@ class NikolaReader(docutils.readers.standalone.Reader): def add_node(node, visit_function=None, depart_function=None): - """ - Register a Docutils node class. + """Register a Docutils node class. + This function is completely optional. It is a same concept as `Sphinx add_node function <http://sphinx-doc.org/ext/appapi.html#sphinx.application.Sphinx.add_node>`_. @@ -236,8 +238,8 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput, writer_name='html', settings=None, settings_spec=None, settings_overrides=None, config_section=None, enable_exit_status=None, logger=None, l_add_ln=0, transforms=None): - """ - Set up & run a `Publisher`, and return a dictionary of document parts. + """Set up & run a ``Publisher``, and return a dictionary of document parts. + Dictionary keys are the names of parts, and values are Unicode strings; encoding is up to the client. For programmatic use with string I/O. diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin index 3e27a25..438abe4 100644 --- a/nikola/plugins/compile/rest/chart.plugin +++ b/nikola/plugins/compile/rest/chart.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_chart -Module = chart +name = rest_chart +module = chart + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Chart directive based in PyGal +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Chart directive based in PyGal diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py index 59b9dc7..88fdff3 100644 --- a/nikola/plugins/compile/rest/chart.py +++ b/nikola/plugins/compile/rest/chart.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Chart directive for reSTructuredText.""" + from ast import literal_eval from docutils import nodes @@ -42,9 +44,12 @@ _site = None class Plugin(RestExtension): + """Plugin for chart role.""" + name = "rest_chart" def set_site(self, site): + """Set Nikola site.""" global _site _site = self.site = site directives.register_directive('chart', Chart) @@ -52,17 +57,18 @@ class Plugin(RestExtension): class Chart(Directive): - """ Restructured text extension for inserting charts as SVG - Usage: - .. chart:: Bar - :title: 'Browser usage evolution (in %)' - :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"] + """reStructuredText extension for inserting charts as SVG. + + Usage: + .. chart:: Bar + :title: 'Browser usage evolution (in %)' + :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"] - 'Firefox', [None, None, 0, 16.6, 25, 31] - 'Chrome', [None, None, None, None, None, None] - 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6] - 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4] + 'Firefox', [None, None, 0, 16.6, 25, 31] + 'Chrome', [None, None, None, None, None, None] + 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6] + 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4] """ has_content = True @@ -129,6 +135,7 @@ class Chart(Directive): } def run(self): + """Run the directive.""" if pygal is None: msg = req_missing(['pygal'], 'use the Chart directive', optional=True) return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin index 1984f52..facdd03 100644 --- a/nikola/plugins/compile/rest/doc.plugin +++ b/nikola/plugins/compile/rest/doc.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_doc -Module = doc +name = rest_doc +module = doc + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Manuel Kaufmann -Version = 0.1 -Website = http://getnikola.com -Description = Role to link another page / post from the blog +author = Manuel Kaufmann +version = 0.1 +website = http://getnikola.com +description = Role to link another page / post from the blog diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py index 703c234..99cce81 100644 --- a/nikola/plugins/compile/rest/doc.py +++ b/nikola/plugins/compile/rest/doc.py @@ -24,6 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""reST role for linking to other documents.""" from docutils import nodes from docutils.parsers.rst import roles @@ -34,9 +35,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for doc role.""" + name = 'rest_doc' def set_site(self, site): + """Set Nikola site.""" self.site = site roles.register_canonical_role('doc', doc_role) doc_role.site = site @@ -45,7 +49,7 @@ class Plugin(RestExtension): def doc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - + """Handle the doc role.""" # 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 @@ -82,5 +86,6 @@ def doc_role(name, rawtext, text, lineno, inliner, def make_link_node(rawtext, text, url, options): + """Make a reST link node.""" node = nodes.reference(rawtext, text, refuri=url, *options) return node diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin index 8f498ec..9fa2e82 100644 --- a/nikola/plugins/compile/rest/gist.plugin +++ b/nikola/plugins/compile/rest/gist.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_gist -Module = gist +name = rest_gist +module = gist + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Gist directive +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Gist directive diff --git a/nikola/plugins/compile/rest/gist.py b/nikola/plugins/compile/rest/gist.py index ab4d56d..736ee37 100644 --- a/nikola/plugins/compile/rest/gist.py +++ b/nikola/plugins/compile/rest/gist.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # This file is public domain according to its author, Brian Hsu +"""Gist directive for reStructuredText.""" + import requests from docutils.parsers.rst import Directive, directives from docutils import nodes @@ -10,26 +12,28 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for gist directive.""" + name = "rest_gist" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('gist', GitHubGist) return super(Plugin, self).set_site(site) class GitHubGist(Directive): - """ Embed GitHub Gist. - - Usage: - .. gist:: GIST_ID + """Embed GitHub Gist. - or + Usage: - .. gist:: GIST_URL + .. gist:: GIST_ID + or + .. gist:: GIST_URL """ required_arguments = 1 @@ -39,10 +43,12 @@ class GitHubGist(Directive): has_content = False def get_raw_gist_with_filename(self, gistID, filename): + """Get raw gist text for a filename.""" url = '/'.join(("https://gist.github.com/raw", gistID, filename)) return requests.get(url).text def get_raw_gist(self, gistID): + """Get raw gist text.""" url = "https://gist.github.com/raw/{0}".format(gistID) try: return requests.get(url).text @@ -50,6 +56,7 @@ class GitHubGist(Directive): raise self.error('Cannot get gist for url={0}'.format(url)) def run(self): + """Run the gist directive.""" if 'https://' in self.arguments[0]: gistID = self.arguments[0].split('/')[-1].strip() else: diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin index 4c9883e..85c780f 100644 --- a/nikola/plugins/compile/rest/listing.plugin +++ b/nikola/plugins/compile/rest/listing.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_listing -Module = listing +name = rest_listing +module = listing + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Extension for source listings +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Extension for source listings diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index b8340cf..4871bf3 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -25,7 +25,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" Define and register a listing directive using the existing CodeBlock """ +"""Define and register a listing directive using the existing CodeBlock.""" from __future__ import unicode_literals @@ -55,7 +55,9 @@ from nikola.plugin_categories import RestExtension # A sanitized version of docutils.parsers.rst.directives.body.CodeBlock. class CodeBlock(Directive): + """Parse and mark up content of a code block.""" + optional_arguments = 1 option_spec = {'class': directives.class_option, 'name': directives.unchanged, @@ -65,6 +67,7 @@ class CodeBlock(Directive): has_content = True def run(self): + """Run code block directive.""" self.assert_has_content() if 'linenos' in self.options: @@ -124,9 +127,12 @@ docutils.parsers.rst.directives.misc.CodeBlock = CodeBlock class Plugin(RestExtension): + """Plugin for listing directive.""" + name = "rest_listing" def set_site(self, site): + """Set Nikola site.""" self.site = site # Even though listings don't use CodeBlock anymore, I am # leaving these to make the code directive work with @@ -146,7 +152,8 @@ listing_spec['linenos'] = directives.unchanged class Listing(Include): - """ listing directive: create a highlighted block of code from a file in listings/ + + """Create a highlighted block of code from a file in listings/. Usage: @@ -154,12 +161,14 @@ class Listing(Include): :number-lines: """ + has_content = False required_arguments = 1 optional_arguments = 1 option_spec = listing_spec def run(self): + """Run listing directive.""" _fname = self.arguments.pop(0) fname = _fname.replace('/', os.sep) lang = self.arguments.pop(0) @@ -185,9 +194,9 @@ class Listing(Include): return generated_nodes def get_code_from_file(self, data): - """ Create CodeBlock nodes from file object content """ + """Create CodeBlock nodes from file object content.""" return super(Listing, self).run() def assert_has_content(self): - """ Listing has no content, override check from superclass """ + """Listing has no content, override check from superclass.""" pass diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin index 5f5276b..9803c8f 100644 --- a/nikola/plugins/compile/rest/media.plugin +++ b/nikola/plugins/compile/rest/media.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_media -Module = media +name = rest_media +module = media + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Directive to support oembed via micawber +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Directive to support oembed via micawber diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py index 0363d28..345e331 100644 --- a/nikola/plugins/compile/rest/media.py +++ b/nikola/plugins/compile/rest/media.py @@ -24,6 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Media directive for reStructuredText.""" from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -40,21 +41,27 @@ from nikola.utils import req_missing class Plugin(RestExtension): + """Plugin for reST media directive.""" + name = "rest_media" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('media', Media) return super(Plugin, self).set_site(site) class Media(Directive): - """ Restructured text extension for inserting any sort of media using micawber.""" + + """reST extension for inserting any sort of media using micawber.""" + has_content = False required_arguments = 1 optional_arguments = 999 def run(self): + """Run media directive.""" if micawber is None: msg = req_missing(['micawber'], 'use the media directive', optional=True) return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin index 82450a0..48969bf 100644 --- a/nikola/plugins/compile/rest/post_list.plugin +++ b/nikola/plugins/compile/rest/post_list.plugin @@ -1,9 +1,14 @@ [Core] -Name = rest_post_list -Module = post_list +name = rest_post_list +module = post_list + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Udo Spallek -Version = 0.1 -Website = http://getnikola.com -Description = Includes a list of posts with tag and slide based filters. +author = Udo Spallek +version = 0.1 +website = http://getnikola.com +description = Includes a list of posts with tag and slide based filters. + diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py index ddbd82d..a22ee85 100644 --- a/nikola/plugins/compile/rest/post_list.py +++ b/nikola/plugins/compile/rest/post_list.py @@ -23,6 +23,9 @@ # 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. + +"""Post list directive for reStructuredText.""" + from __future__ import unicode_literals import os @@ -40,9 +43,13 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + + """Plugin for reST post-list directive.""" + name = "rest_post_list" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('post-list', PostList) PostList.site = site @@ -50,14 +57,15 @@ class Plugin(RestExtension): class PostList(Directive): - """ + + """Provide a reStructuredText directive to create a list of posts. + Post List ========= :Directive Arguments: None. - :Directive Options: lang, start, stop, reverse, sort, tags, template, id + :Directive Options: lang, start, stop, reverse, sort, tags, categories, slugs, all, template, id :Directive Content: None. - Provides a reStructuredText directive to create a list of posts. The posts appearing in the list can be filtered by options. *List slicing* is provided with the *start*, *stop* and *reverse* options. @@ -87,6 +95,10 @@ class PostList(Directive): Filter posts to show only posts having at least one of the ``tags``. Defaults to None. + ``categories`` : string [, string...] + Filter posts to show only posts having one of the ``categories``. + Defaults to None. + ``slugs`` : string [, string...] Filter posts to show only posts having at least one of the ``slugs``. Defaults to None. @@ -107,12 +119,14 @@ class PostList(Directive): A manual id for the post list. Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex. """ + option_spec = { 'start': int, 'stop': int, 'reverse': directives.flag, 'sort': directives.unchanged, 'tags': directives.unchanged, + 'categories': directives.unchanged, 'slugs': directives.unchanged, 'all': directives.flag, 'lang': directives.unchanged, @@ -121,11 +135,14 @@ class PostList(Directive): } def run(self): + """Run post-list directive.""" start = self.options.get('start') stop = self.options.get('stop') reverse = self.options.get('reverse', False) tags = self.options.get('tags') tags = [t.strip().lower() for t in tags.split(',')] if tags else [] + categories = self.options.get('categories') + categories = [c.strip().lower() for c in categories.split(',')] if categories else [] slugs = self.options.get('slugs') slugs = [s.strip() for s in slugs.split(',')] if slugs else [] show_all = self.options.get('all', False) @@ -145,6 +162,9 @@ class PostList(Directive): else: timeline = [p for p in self.site.timeline if p.use_in_feeds] + if categories: + timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories] + for post in timeline: if tags: cont = True diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin index cee4b06..5c05b89 100644 --- a/nikola/plugins/compile/rest/slides.plugin +++ b/nikola/plugins/compile/rest/slides.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_slides -Module = slides +name = rest_slides +module = slides + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Slides directive +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Slides directive diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py index 7826f6a..2522e55 100644 --- a/nikola/plugins/compile/rest/slides.py +++ b/nikola/plugins/compile/rest/slides.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Slides directive for reStructuredText.""" + from __future__ import unicode_literals import uuid @@ -36,9 +38,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for reST slides directive.""" + name = "rest_slides" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('slides', Slides) Slides.site = site @@ -46,11 +51,14 @@ class Plugin(RestExtension): class Slides(Directive): - """ Restructured text extension for inserting slideshows.""" + + """reST extension for inserting slideshows.""" + has_content = True def run(self): - if len(self.content) == 0: + """Run the slides directive.""" + if len(self.content) == 0: # pragma: no cover return if self.site.invariant: # for testing purposes diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin index 1d31a8f..75469e4 100644 --- a/nikola/plugins/compile/rest/soundcloud.plugin +++ b/nikola/plugins/compile/rest/soundcloud.plugin @@ -1,10 +1,14 @@ [Core] -Name = rest_soundcloud -Module = soundcloud +name = rest_soundcloud +module = soundcloud + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://getnikola.com -Description = Soundcloud directive +author = Roberto Alsina +version = 0.1 +website = http://getnikola.com +description = Soundcloud directive diff --git a/nikola/plugins/compile/rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py index a26806c..30134a9 100644 --- a/nikola/plugins/compile/rest/soundcloud.py +++ b/nikola/plugins/compile/rest/soundcloud.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +"""SoundCloud directive for reStructuredText.""" from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -10,9 +11,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for soundclound directive.""" + name = "rest_soundcloud" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('soundcloud', SoundCloud) directives.register_directive('soundcloud_playlist', SoundCloudPlaylist) @@ -27,7 +31,8 @@ src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/{preslug}/"" class SoundCloud(Directive): - """ Restructured text extension for inserting SoundCloud embedded music + + """reST extension for inserting SoundCloud embedded music. Usage: .. soundcloud:: <sound id> @@ -35,6 +40,7 @@ class SoundCloud(Directive): :width: 600 """ + has_content = True required_arguments = 1 option_spec = { @@ -44,7 +50,7 @@ class SoundCloud(Directive): preslug = "tracks" def run(self): - """ Required by the Directive interface. Create docutils nodes """ + """Run the soundcloud directive.""" self.check_content() options = { 'sid': self.arguments[0], @@ -56,12 +62,15 @@ class SoundCloud(Directive): return [nodes.raw('', CODE.format(**options), format='html')] def check_content(self): - """ Emit a deprecation warning if there is content """ - if self.content: + """Emit a deprecation warning if there is content.""" + if self.content: # pragma: no cover raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " "use ':key: value' instead") class SoundCloudPlaylist(SoundCloud): + + """reST directive for SoundCloud playlists.""" + preslug = "playlists" diff --git a/nikola/plugins/compile/rest/thumbnail.plugin b/nikola/plugins/compile/rest/thumbnail.plugin index 3b73340..0084310 100644 --- a/nikola/plugins/compile/rest/thumbnail.plugin +++ b/nikola/plugins/compile/rest/thumbnail.plugin @@ -1,9 +1,14 @@ [Core] -Name = rest_thumbnail -Module = thumbnail +name = rest_thumbnail +module = thumbnail + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Author = Pelle Nilsson -Version = 0.1 -Website = http://getnikola.com -Description = reST directive to facilitate enlargeable images with thumbnails +author = Pelle Nilsson +version = 0.1 +website = http://getnikola.com +description = reST directive to facilitate enlargeable images with thumbnails + diff --git a/nikola/plugins/compile/rest/thumbnail.py b/nikola/plugins/compile/rest/thumbnail.py index 5388d8d..1fae06c 100644 --- a/nikola/plugins/compile/rest/thumbnail.py +++ b/nikola/plugins/compile/rest/thumbnail.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Thumbnail directive for reStructuredText.""" + import os from docutils.parsers.rst import directives @@ -34,9 +36,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for thumbnail directive.""" + name = "rest_thumbnail" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('thumbnail', Thumbnail) return super(Plugin, self).set_site(site) @@ -44,10 +49,14 @@ class Plugin(RestExtension): class Thumbnail(Figure): + """Thumbnail directive for reST.""" + def align(argument): + """Return thumbnail alignment.""" return directives.choice(argument, Image.align_values) def figwidth_value(argument): + """Return figure width.""" if argument.lower() == 'image': return 'image' else: @@ -59,6 +68,7 @@ class Thumbnail(Figure): has_content = True def run(self): + """Run the thumbnail directive.""" uri = directives.uri(self.arguments[0]) self.options['target'] = uri self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin index e0ff3f1..688f981 100644 --- a/nikola/plugins/compile/rest/vimeo.plugin +++ b/nikola/plugins/compile/rest/vimeo.plugin @@ -1,7 +1,11 @@ [Core] -Name = rest_vimeo -Module = vimeo +name = rest_vimeo +module = vimeo + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Description = Vimeo directive +description = Vimeo directive diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index bc44b0e..c694a87 100644 --- a/nikola/plugins/compile/rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -24,6 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Vimeo directive for reStructuredText.""" from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -37,9 +38,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for vimeo reST directive.""" + name = "rest_vimeo" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('vimeo', Vimeo) return super(Plugin, self).set_site(site) @@ -56,14 +60,16 @@ VIDEO_DEFAULT_WIDTH = 281 class Vimeo(Directive): - """ Restructured text extension for inserting vimeo embedded videos - Usage: - .. vimeo:: 20241459 - :height: 400 - :width: 600 + """reST extension for inserting vimeo embedded videos. + + Usage: + .. vimeo:: 20241459 + :height: 400 + :width: 600 """ + has_content = True required_arguments = 1 option_spec = { @@ -75,6 +81,7 @@ class Vimeo(Directive): request_size = True def run(self): + """Run the vimeo directive.""" self.check_content() options = { 'vimeo_id': self.arguments[0], @@ -90,9 +97,11 @@ class Vimeo(Directive): return [nodes.raw('', CODE.format(**options), format='html')] def check_modules(self): + """Check modules.""" return None def set_video_size(self): + """Set video size.""" # Only need to make a connection if width and height aren't provided if 'height' not in self.options or 'width' not in self.options: self.options['height'] = VIDEO_DEFAULT_HEIGHT @@ -111,6 +120,7 @@ class Vimeo(Directive): pass def check_content(self): + """Check if content exists.""" if self.content: raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin index 01275be..5fbd67b 100644 --- a/nikola/plugins/compile/rest/youtube.plugin +++ b/nikola/plugins/compile/rest/youtube.plugin @@ -1,8 +1,12 @@ [Core] -Name = rest_youtube -Module = youtube +name = rest_youtube +module = youtube + +[Nikola] +compiler = rest +plugincategory = CompilerExtension [Documentation] -Version = 0.1 -Description = Youtube directive +version = 0.1 +description = Youtube directive diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py index 7c6bba1..6c5c211 100644 --- a/nikola/plugins/compile/rest/youtube.py +++ b/nikola/plugins/compile/rest/youtube.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""YouTube directive for reStructuredText.""" + from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -33,9 +35,12 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): + """Plugin for the youtube directive.""" + name = "rest_youtube" def set_site(self, site): + """Set Nikola site.""" self.site = site directives.register_directive('youtube', Youtube) return super(Plugin, self).set_site(site) @@ -49,7 +54,8 @@ src="//www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" class Youtube(Directive): - """ Restructured text extension for inserting youtube embedded videos + + """reST extension for inserting youtube embedded videos. Usage: .. youtube:: lyViVmaBQDg @@ -57,6 +63,7 @@ class Youtube(Directive): :width: 600 """ + has_content = True required_arguments = 1 option_spec = { @@ -65,6 +72,7 @@ class Youtube(Directive): } def run(self): + """Run the youtube directive.""" self.check_content() options = { 'yid': self.arguments[0], @@ -75,7 +83,8 @@ class Youtube(Directive): return [nodes.raw('', CODE.format(**options), format='html')] def check_content(self): - if self.content: + """Check if content exists.""" + if self.content: # pragma: no cover raise self.warning("This directive does not accept content. The " "'key=value' format for options is deprecated, " "use ':key: value' instead") diff --git a/nikola/plugins/loghandler/smtp.plugin b/nikola/plugins/loghandler/smtp.plugin deleted file mode 100644 index 38c1d96..0000000 --- a/nikola/plugins/loghandler/smtp.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = smtp -Module = smtp - -[Documentation] -Author = Daniel Devine -Version = 1.0 -Website = http://getnikola.com -Description = Log over smtp (email). diff --git a/nikola/plugins/loghandler/smtp.py b/nikola/plugins/loghandler/smtp.py deleted file mode 100644 index 146a658..0000000 --- a/nikola/plugins/loghandler/smtp.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2015 Daniel Devine and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from nikola.plugin_categories import SignalHandler -from blinker import signal -import logbook - - -class SmtpHandler(SignalHandler): - name = 'smtp' - - def attach_handler(self, sender): - """Add the handler to a list of handlers that are attached when get_logger() is called..""" - smtpconf = self.site.config.get('LOGGING_HANDLERS').get('smtp') - if smtpconf: - smtpconf['format_string'] = '''\ -Subject: {record.level_name}: {record.channel} - -{record.message} -''' - self.site.loghandlers.append(logbook.MailHandler( - smtpconf.pop('from_addr'), - smtpconf.pop('recipients'), - **smtpconf - )) - - def set_site(self, site): - self.site = site - - ready = signal('sighandlers_loaded') - ready.connect(self.attach_handler) diff --git a/nikola/plugins/loghandler/stderr.plugin b/nikola/plugins/loghandler/stderr.plugin deleted file mode 100644 index 6c20ea1..0000000 --- a/nikola/plugins/loghandler/stderr.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = stderr -Module = stderr - -[Documentation] -Author = Daniel Devine -Version = 1.0 -Website = http://getnikola.com -Description = Log to stderr, the default logger. diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py deleted file mode 100644 index 79ace68..0000000 --- a/nikola/plugins/loghandler/stderr.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2015 Daniel Devine and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from nikola.plugin_categories import SignalHandler -from blinker import signal -import os - -from nikola import DEBUG -from nikola.utils import ColorfulStderrHandler - - -class StderrHandler(SignalHandler): - """Logs messages to stderr.""" - name = 'stderr' - - def attach_handler(self, sender): - """Attach the handler to the logger.""" - conf = self.site.config.get('LOGGING_HANDLERS').get('stderr') - if conf or os.getenv('NIKOLA_DEBUG'): - self.site.loghandlers.append(ColorfulStderrHandler( - # 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}' - )) - - def set_site(self, site): - self.site = site - - ready = signal('sighandlers_loaded') - ready.connect(self.attach_handler) diff --git a/nikola/plugins/loghandler/__init__.py b/nikola/plugins/misc/__init__.py index a1d17a6..c0d8961 100644 --- a/nikola/plugins/loghandler/__init__.py +++ b/nikola/plugins/misc/__init__.py @@ -23,3 +23,5 @@ # 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. + +"""Miscellaneous Nikola plugins.""" diff --git a/nikola/plugins/misc/scan_posts.py b/nikola/plugins/misc/scan_posts.py index a6f04e6..1f4f995 100644 --- a/nikola/plugins/misc/scan_posts.py +++ b/nikola/plugins/misc/scan_posts.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The default post scanner.""" + from __future__ import unicode_literals, print_function import glob import os @@ -35,13 +37,13 @@ from nikola.post import Post class ScanPosts(PostScanner): - """Render pages into output.""" + + """Scan posts in the site.""" name = "scan_posts" def scan(self): """Create list of posts from POSTS and PAGES options.""" - seen = set([]) if not self.site.quiet: print("Scanning posts", end='', file=sys.stderr) diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py index a1d17a6..fd9a48f 100644 --- a/nikola/plugins/task/__init__.py +++ b/nikola/plugins/task/__init__.py @@ -23,3 +23,5 @@ # 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. + +"""Tasks for Nikola.""" diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin index 6687209..25f1195 100644 --- a/nikola/plugins/task/archive.plugin +++ b/nikola/plugins/task/archive.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_archive -Module = archive +name = render_archive +module = archive [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Generates the blog's archive pages. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Generates the blog's archive pages. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index 533be69..126aed4 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render the post archives.""" + import copy import os @@ -35,17 +37,20 @@ from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name class Archive(Task): + """Render the post archives.""" name = "render_archive" def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('archive', self.archive_path) site.register_path_handler('archive_atom', self.archive_atom_path) return super(Archive, self).set_site(site) def _prepare_task(self, kw, name, lang, posts, items, template_name, title, deps_translatable=None): + """Prepare an archive task.""" # name: used to build permalink and destination # posts, items: posts or items; only one of them should be used, # the other be None @@ -53,17 +58,20 @@ class Archive(Task): # title: the (translated) title for the generated page # deps_translatable: dependencies (None if not added) assert posts is not None or items is not None - + task_cfg = [copy.copy(kw)] context = {} context["lang"] = lang context["title"] = title context["permalink"] = self.site.link("archive", name, lang) + context["pagekind"] = ["list", "archive_page"] if posts is not None: context["posts"] = posts - n = len(posts) + # Depend on all post metadata because it can be used in templates (Issue #1931) + task_cfg.append([repr(p) for p in posts]) else: + # Depend on the content of items, to rebuild if links change (Issue #1931) context["items"] = items - n = len(items) + task_cfg.append(items) task = self.site.generic_post_list_renderer( lang, [], @@ -73,7 +81,7 @@ class Archive(Task): context, ) - task_cfg = {1: copy.copy(kw), 2: n} + task_cfg = {i: x for i, x in enumerate(task_cfg)} if deps_translatable is not None: task_cfg[3] = deps_translatable task['uptodate'] = task['uptodate'] + [config_changed(task_cfg, 'nikola.plugins.task.archive')] @@ -81,6 +89,7 @@ class Archive(Task): return task def _generate_posts_task(self, kw, name, lang, posts, title, deps_translatable=None): + """Genereate a task for an archive with posts.""" posts = sorted(posts, key=lambda a: a.date) posts.reverse() if kw['archives_are_indexes']: @@ -97,13 +106,15 @@ class Archive(Task): uptodate = [] if deps_translatable is not None: uptodate += [config_changed(deps_translatable, 'nikola.plugins.task.archive')] + context = {"archive_name": name, + "is_feed_stale": kw["is_feed_stale"], + "pagekind": ["index", "archive_page"]} yield self.site.generic_index_renderer( lang, posts, title, "archiveindex.tmpl", - {"archive_name": name, - "is_feed_stale": kw["is_feed_stale"]}, + context, kw, str(self.name), page_link, @@ -113,6 +124,7 @@ class Archive(Task): yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable) def gen_tasks(self): + """Generate archive tasks.""" kw = { "messages": self.site.MESSAGES, "translations": self.site.config['TRANSLATIONS'], @@ -211,6 +223,7 @@ class Archive(Task): yield self._prepare_task(kw, None, lang, None, items, "list.tmpl", kw["messages"][lang]["Archive"]) def archive_path(self, name, lang, is_feed=False): + """Return archive paths.""" if is_feed: extension = ".atom" archive_file = os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0] + extension @@ -228,4 +241,5 @@ class Archive(Task): archive_file] if _f] def archive_atom_path(self, name, lang): + """Return Atom archive paths.""" return self.archive_path(name, lang, is_feed=True) diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin index 3fe049b..ca997d0 100644 --- a/nikola/plugins/task/bundles.plugin +++ b/nikola/plugins/task/bundles.plugin @@ -1,10 +1,13 @@ [Core] -Name = create_bundles -Module = bundles +name = create_bundles +module = bundles [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Theme bundles using WebAssets +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Theme bundles using WebAssets + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py index 6f88d0c..b9c57b9 100644 --- a/nikola/plugins/task/bundles.py +++ b/nikola/plugins/task/bundles.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Bundle assets using WebAssets.""" + from __future__ import unicode_literals import os @@ -38,12 +40,14 @@ from nikola import utils class BuildBundles(LateTask): + """Bundle assets using WebAssets.""" name = "create_bundles" def set_site(self, site): - self.logger = utils.get_logger('bundles', site.loghandlers) + """Set Nikola site.""" + self.logger = utils.get_logger('bundles', utils.STDERR_HANDLER) if webassets is None and site.config['USE_BUNDLES']: utils.req_missing(['webassets'], 'USE_BUNDLES', optional=True) self.logger.warn('Setting USE_BUNDLES to False.') @@ -52,7 +56,6 @@ class BuildBundles(LateTask): def gen_tasks(self): """Bundle assets using WebAssets.""" - kw = { 'filters': self.site.config['FILTERS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin index 0530ebf..c182150 100644 --- a/nikola/plugins/task/copy_assets.plugin +++ b/nikola/plugins/task/copy_assets.plugin @@ -1,10 +1,13 @@ [Core] -Name = copy_assets -Module = copy_assets +name = copy_assets +module = copy_assets [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Copy theme assets into output. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Copy theme assets into output. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py index a72bfdf..58521d4 100644 --- a/nikola/plugins/task/copy_assets.py +++ b/nikola/plugins/task/copy_assets.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Copy theme assets into output.""" + from __future__ import unicode_literals import io @@ -34,6 +36,7 @@ from nikola import utils class CopyAssets(Task): + """Copy theme assets into output.""" name = "copy_assets" @@ -44,7 +47,6 @@ class CopyAssets(Task): If a file is present on two themes, use the version from the "youngest" theme. """ - kw = { "themes": self.site.THEMES, "files_folders": self.site.config['FILES_FOLDERS'], diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin index 073676b..ce8f5d0 100644 --- a/nikola/plugins/task/copy_files.plugin +++ b/nikola/plugins/task/copy_files.plugin @@ -1,10 +1,13 @@ [Core] -Name = copy_files -Module = copy_files +name = copy_files +module = copy_files [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Copy static files into the output. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Copy static files into the output. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py index 9a039f1..1232248 100644 --- a/nikola/plugins/task/copy_files.py +++ b/nikola/plugins/task/copy_files.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Copy static files into the output folder.""" + import os from nikola.plugin_categories import Task @@ -31,13 +33,13 @@ from nikola import utils class CopyFiles(Task): + """Copy static files into the output folder.""" name = "copy_files" def gen_tasks(self): """Copy static files into the output folder.""" - kw = { 'files_folders': self.site.config['FILES_FOLDERS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin index 73085cd..9d3fa28 100644 --- a/nikola/plugins/task/galleries.plugin +++ b/nikola/plugins/task/galleries.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_galleries -Module = galleries +name = render_galleries +module = galleries [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create image galleries automatically. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create image galleries automatically. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index e887f18..c0df4a4 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -24,10 +24,12 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render image galleries.""" + from __future__ import unicode_literals -import io import datetime import glob +import io import json import mimetypes import os @@ -55,17 +57,19 @@ _image_size_cache = {} class Galleries(Task, ImageProcessor): + """Render image galleries.""" name = 'render_galleries' dates = {} def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('gallery', self.gallery_path) site.register_path_handler('gallery_global', self.gallery_global_path) site.register_path_handler('gallery_rss', self.gallery_rss_path) - self.logger = utils.get_logger('render_galleries', site.loghandlers) + self.logger = utils.get_logger('render_galleries', utils.STDERR_HANDLER) self.kw = { 'thumbnail_size': site.config['THUMBNAIL_SIZE'], @@ -118,17 +122,20 @@ class Galleries(Task, ImageProcessor): sys.exit(1) def gallery_path(self, name, lang): + """Return a gallery path.""" gallery_path = self._find_gallery_path(name) return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + gallery_path.split(os.sep) + [self.site.config['INDEX_FILE']] if _f] def gallery_global_path(self, name, lang): + """Return the global gallery path, which contains images.""" gallery_path = self._find_gallery_path(name) return [_f for _f in gallery_path.split(os.sep) + [self.site.config['INDEX_FILE']] if _f] def gallery_rss_path(self, name, lang): + """Return path to the RSS file for a gallery.""" gallery_path = self._find_gallery_path(name) return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + gallery_path.split(os.sep) + @@ -136,7 +143,6 @@ class Galleries(Task, ImageProcessor): def gen_tasks(self): """Render image galleries.""" - self.image_ext_list = self.image_ext_list_builtin self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', [])) @@ -183,11 +189,13 @@ class Galleries(Task, ImageProcessor): crumbs = utils.get_crumbs(gallery, index_folder=self) - # Create index.html for each language for lang in self.kw['translations']: # save navigation links as dependencies self.kw['navigation_links|{0}'.format(lang)] = self.kw['global_context']['navigation_links'](lang) + # Create index.html for each language + for lang in self.kw['translations']: + dst = os.path.join( self.kw['output_folder'], self.site.path("gallery", gallery, lang)) @@ -238,6 +246,7 @@ class Galleries(Task, ImageProcessor): context["permalink"] = self.site.link("gallery", gallery, lang) context["enable_comments"] = self.kw['comments_in_galleries'] context["thumbnail_size"] = self.kw["thumbnail_size"] + context["pagekind"] = ["gallery_front"] if post: yield { @@ -246,7 +255,7 @@ class Galleries(Task, ImageProcessor): 'targets': [post.translated_base_path(lang)], 'file_dep': post.fragment_deps(lang), 'actions': [(post.compile, [lang])], - 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:post')] + post.fragment_deps_uptodate(lang) + 'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:post')] + post.fragment_deps_uptodate(lang) } context['post'] = post else: @@ -259,6 +268,8 @@ class Galleries(Task, ImageProcessor): file_dep += [post.translated_base_path(l) for l in self.kw['translations']] file_dep_dest += [post.translated_base_path(l) for l in self.kw['translations']] + context["pagekind"] = ["gallery_page"] + yield utils.apply_filters({ 'basename': self.name, 'name': dst, @@ -268,14 +279,14 @@ class Galleries(Task, ImageProcessor): (self.render_gallery_index, ( template_name, dst, - context, + context.copy(), dest_img_list, img_titles, thumbs, file_dep))], 'clean': True, 'uptodate': [utils.config_changed({ - 1: self.kw, + 1: self.kw.copy(), 2: self.site.config["COMMENTS_IN_GALLERIES"], 3: context.copy(), }, 'nikola.plugins.task.galleries:gallery')], @@ -305,21 +316,19 @@ class Galleries(Task, ImageProcessor): ))], 'clean': True, 'uptodate': [utils.config_changed({ - 1: self.kw, + 1: self.kw.copy(), }, 'nikola.plugins.task.galleries:rss')], }, self.kw['filters']) def find_galleries(self): - """Find all galleries to be processed according to conf.py""" - + """Find all galleries to be processed according to conf.py.""" self.gallery_list = [] for input_folder, output_folder in self.kw['gallery_folders'].items(): for root, dirs, files in os.walk(input_folder, followlinks=True): self.gallery_list.append((root, input_folder, output_folder)) def create_galleries_paths(self): - """Given a list of galleries, puts their paths into self.gallery_links.""" - + """Given a list of galleries, put their paths into self.gallery_links.""" # gallery_path is "gallery/foo/name" self.proper_gallery_links = dict() self.improper_gallery_links = dict() @@ -350,7 +359,6 @@ class Galleries(Task, ImageProcessor): def create_galleries(self): """Given a list of galleries, create the output folders.""" - # gallery_path is "gallery/foo/name" for gallery_path, input_folder, _ in self.gallery_list: # have to use dirname because site.path returns .../index.html @@ -366,12 +374,11 @@ class Galleries(Task, ImageProcessor): 'actions': [(utils.makedirs, (output_gallery,))], 'targets': [output_gallery], 'clean': True, - 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:mkdir')], + 'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:mkdir')], } def parse_index(self, gallery, input_folder, output_folder): - """Returns a Post object if there is an index.txt.""" - + """Return a Post object if there is an index.txt.""" index_path = os.path.join(gallery, "index.txt") destination = os.path.join( self.kw["output_folder"], output_folder, @@ -397,6 +404,7 @@ class Galleries(Task, ImageProcessor): return post def get_excluded_images(self, gallery_path): + """Get list of excluded images.""" exclude_path = os.path.join(gallery_path, "exclude.meta") try: @@ -409,7 +417,7 @@ class Galleries(Task, ImageProcessor): return excluded_image_list def get_image_list(self, gallery_path): - + """Get list of included images.""" # Gather image_list contains "gallery/name/image_name.jpg" image_list = [] @@ -424,6 +432,7 @@ class Galleries(Task, ImageProcessor): return image_list def create_target_images(self, img, input_path): + """Copy images to output.""" gallery_name = os.path.dirname(img) output_gallery = os.path.dirname( os.path.join( @@ -473,6 +482,7 @@ class Galleries(Task, ImageProcessor): }, self.kw['filters']) def remove_excluded_image(self, img, input_folder): + """Remove excluded images.""" # Remove excluded images # img is something like input_folder/demo/tesla2_lg.jpg so it's the *source* path # and we should remove both the large and thumbnail *destination* paths @@ -493,7 +503,7 @@ class Galleries(Task, ImageProcessor): (utils.remove_file, (thumb_path,)) ], 'clean': True, - 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_thumb')], + 'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:clean_thumb')], }, self.kw['filters']) yield utils.apply_filters({ @@ -503,7 +513,7 @@ class Galleries(Task, ImageProcessor): (utils.remove_file, (img_path,)) ], 'clean': True, - 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_file')], + 'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:clean_file')], }, self.kw['filters']) def render_gallery_index( @@ -516,7 +526,6 @@ class Galleries(Task, ImageProcessor): thumbs, file_dep): """Build the gallery index.""" - # The photo array needs to be created here, because # it relies on thumbnails already being created on # output @@ -543,7 +552,7 @@ class Galleries(Task, ImageProcessor): }, }) context['photo_array'] = photo_array - context['photo_array_json'] = json.dumps(photo_array) + context['photo_array_json'] = json.dumps(photo_array, sort_keys=True) self.site.render_template(template_name, output_name, context) def gallery_rss(self, img_list, dest_img_list, img_titles, lang, permalink, output_path, title): @@ -552,7 +561,6 @@ class Galleries(Task, ImageProcessor): This doesn't use generic_rss_renderer because it doesn't involve Post objects. """ - def make_url(url): return urljoin(self.site.config['BASE_URL'], url.lstrip('/')) diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin index 4867fd6..7834d22 100644 --- a/nikola/plugins/task/gzip.plugin +++ b/nikola/plugins/task/gzip.plugin @@ -1,10 +1,13 @@ [Core] -Name = gzip -Module = gzip +name = gzip +module = gzip [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create gzipped copies of files +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create gzipped copies of files + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py index 5799839..cf16f63 100644 --- a/nikola/plugins/task/gzip.py +++ b/nikola/plugins/task/gzip.py @@ -35,12 +35,14 @@ from nikola.plugin_categories import TaskMultiplier class GzipFiles(TaskMultiplier): + """If appropiate, create tasks to create gzipped versions of files.""" name = "gzip" is_default = True def process(self, task, prefix): + """Process tasks.""" if not self.site.config['GZIP_FILES']: return [] if task.get('name') is None: @@ -70,6 +72,7 @@ class GzipFiles(TaskMultiplier): def create_gzipped_copy(in_path, out_path, command=None): + """Create gzipped copy of in_path and save it as out_path.""" if command: subprocess.check_call(shlex.split(command.format(filename=in_path))) else: diff --git a/nikola/plugins/task/indexes.plugin b/nikola/plugins/task/indexes.plugin index 5d2bf5a..d9b0e5f 100644 --- a/nikola/plugins/task/indexes.plugin +++ b/nikola/plugins/task/indexes.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_indexes -Module = indexes +name = render_indexes +module = indexes [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Generates the blog's index pages. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Generates the blog's index pages. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index 03d36b1..c02818e 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render the blog indexes.""" + from __future__ import unicode_literals from collections import defaultdict import os @@ -33,16 +35,19 @@ from nikola import utils class Indexes(Task): + """Render the blog indexes.""" name = "render_indexes" def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('index', self.index_path) site.register_path_handler('index_atom', self.index_atom_path) return super(Indexes, self).set_site(site) def gen_tasks(self): + """Render the blog indexes.""" self.site.scan_posts() yield self.group_task() @@ -80,7 +85,10 @@ class Indexes(Task): indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang) self.number_of_pages[lang] = (len(filtered_posts) + kw['index_display_post_count'] - 1) // kw['index_display_post_count'] - yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, {}, kw, 'render_indexes', page_link, page_path) + context = {} + context["pagekind"] = ["index"] + + yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path) if not self.site.config["STORY_INDEX"]: return @@ -93,13 +101,17 @@ class Indexes(Task): "strip_indexes": self.site.config['STRIP_INDEXES'], } template_name = "list.tmpl" + index_len = len(kw['index_file']) for lang in kw["translations"]: # Need to group by folder to avoid duplicated tasks (Issue #758) # 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)) + destpath = p.destination_path(lang) + if destpath[-(1 + index_len):] == '/' + kw['index_file']: + destpath = destpath[:-(1 + index_len)] + dirname = os.path.dirname(destpath) groups[dirname].append(p) for dirname, post_list in groups.items(): context = {} @@ -108,10 +120,12 @@ class Indexes(Task): output_name = os.path.join(kw['output_folder'], dirname, kw['index_file']) short_destination = os.path.join(dirname, kw['index_file']) link = short_destination.replace('\\', '/') - index_len = len(kw['index_file']) if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']: link = link[:-index_len] context["permalink"] = link + context["pagekind"] = ["list"] + if dirname == "/": + context["pagekind"].append("front_page") for post in post_list: # If there is an index.html pending to be created from @@ -133,6 +147,7 @@ class Indexes(Task): yield task def index_path(self, name, lang, is_feed=False): + """Return path to an index.""" extension = None if is_feed: extension = ".atom" @@ -149,4 +164,5 @@ class Indexes(Task): extension=extension) def index_atom_path(self, name, lang): + """Return path to an Atom index.""" return self.index_path(name, lang, is_feed=True) diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin index a5ba77a..435234b 100644 --- a/nikola/plugins/task/listings.plugin +++ b/nikola/plugins/task/listings.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_listings -Module = listings +name = render_listings +module = listings [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Render code listings into output +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Render code listings into output + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py index b913330..5f79724 100644 --- a/nikola/plugins/task/listings.py +++ b/nikola/plugins/task/listings.py @@ -24,10 +24,13 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render code listings.""" + from __future__ import unicode_literals, print_function import sys import os +import lxml.html from pygments import highlight from pygments.lexers import get_lexer_for_filename, TextLexer @@ -38,7 +41,8 @@ from nikola import utils class Listings(Task): - """Render pretty listings.""" + + """Render code listings.""" name = "render_listings" @@ -51,6 +55,7 @@ class Listings(Task): self.proper_input_file_mapping[rel_output_name] = rel_output_name def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('listing', self.listing_path) # We need to prepare some things for the listings path handler to work. @@ -105,12 +110,21 @@ class Listings(Task): def gen_tasks(self): """Render pretty code listings.""" - # Things to ignore in listings ignored_extensions = (".pyc", ".pyo") def render_listing(in_name, out_name, input_folder, output_folder, folders=[], files=[]): - if in_name: + needs_ipython_css = False + if in_name and in_name.endswith('.ipynb'): + # Special handling: render ipynbs in listings (Issue #1900) + ipynb_compiler = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler").plugin_object + ipynb_raw = ipynb_compiler.compile_html_string(in_name, True) + ipynb_html = lxml.html.fromstring(ipynb_raw) + # The raw HTML contains garbage (scripts and styles), we can’t leave it in + code = lxml.html.tostring(ipynb_html.xpath('//*[@id="notebook"]')[0], encoding='unicode') + title = os.path.basename(in_name) + needs_ipython_css = True + elif in_name: with open(in_name, 'r') as fd: try: lexer = get_lexer_for_filename(in_name) @@ -149,7 +163,12 @@ class Listings(Task): files, alg=natsort.ns.F | natsort.ns.IC), 'description': title, 'source_link': source_link, + 'pagekind': ['listing'], } + if needs_ipython_css: + # If someone does not have ipynb posts and only listings, we + # need to enable ipynb CSS for ipynb listings. + context['needs_ipython_css'] = True self.site.render_template('listing.tmpl', out_name, context) yield self.group_task() @@ -236,6 +255,7 @@ class Listings(Task): }, self.kw["filters"]) def listing_path(self, namep, lang): + """Return path to a listing.""" namep = namep.replace('/', os.sep) nameh = namep + '.html' for name in (namep, nameh): diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin index 4cad7b7..023d41b 100644 --- a/nikola/plugins/task/pages.plugin +++ b/nikola/plugins/task/pages.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_pages -Module = pages +name = render_pages +module = pages [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create pages in the output. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create pages in the output. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py index d0edb56..e6a8a82 100644 --- a/nikola/plugins/task/pages.py +++ b/nikola/plugins/task/pages.py @@ -24,12 +24,15 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render pages into output.""" + from __future__ import unicode_literals from nikola.plugin_categories import Task from nikola.utils import config_changed class RenderPages(Task): + """Render pages into output.""" name = "render_pages" @@ -49,8 +52,11 @@ class RenderPages(Task): for post in self.site.timeline: 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"]): + if post.is_post: + context = {'pagekind': ['post_page']} + else: + context = {'pagekind': ['story_page']} + for task in self.site.generic_page_renderer(lang, post, kw["filters"], context): task['uptodate'] = task['uptodate'] + [config_changed(kw, 'nikola.plugins.task.pages')] task['basename'] = self.name task['task_dep'] = ['render_posts'] diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin index 707b3c2..79b7c51 100644 --- a/nikola/plugins/task/posts.plugin +++ b/nikola/plugins/task/posts.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_posts -Module = posts +name = render_posts +module = posts [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create HTML fragments out of posts. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create HTML fragments out of posts. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py index d3f17fd..a3a8375 100644 --- a/nikola/plugins/task/posts.py +++ b/nikola/plugins/task/posts.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Build HTML fragments from metadata and text.""" + from copy import copy import os @@ -32,7 +34,7 @@ from nikola import filters, utils def update_deps(post, lang, task): - """Updates file dependencies as they might have been updated during compilation. + """Update file dependencies as they might have been updated during compilation. This is done for example by the ReST page compiler, which writes its dependencies into a .dep file. This file is read and incorporated when calling @@ -42,6 +44,7 @@ def update_deps(post, lang, task): class RenderPosts(Task): + """Build HTML fragments from metadata and text.""" name = "render_posts" @@ -74,7 +77,11 @@ class RenderPosts(Task): deps_dict = copy(kw) deps_dict.pop('timeline') for post in kw['timeline']: - + # Extra config dependencies picked from config + for p in post.fragment_deps(lang): + if p.startswith('####MAGIC####CONFIG:'): + k = p.split('####MAGIC####CONFIG:', 1)[-1] + deps_dict[k] = self.site.config.get(k) dest = post.translated_base_path(lang) file_dep = [p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")] task = { @@ -110,6 +117,7 @@ class RenderPosts(Task): yield utils.apply_filters(task, {os.path.splitext(dest): flist}) def dependence_on_timeline(self, post, lang): + """Check if a post depends on the timeline.""" if "####MAGIC####TIMELINE" not in post.fragment_deps(lang): return True # No dependency on timeline elif self.tl_changed: diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin index 0228c70..c3137b9 100644 --- a/nikola/plugins/task/redirect.plugin +++ b/nikola/plugins/task/redirect.plugin @@ -1,10 +1,13 @@ [Core] -Name = redirect -Module = redirect +name = redirect +module = redirect [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Create redirect pages. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Create redirect pages. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py index 428dd5a..8530f5e 100644 --- a/nikola/plugins/task/redirect.py +++ b/nikola/plugins/task/redirect.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Generate redirections.""" + from __future__ import unicode_literals import os @@ -33,13 +35,13 @@ from nikola import utils class Redirect(Task): - """Generate redirections""" + + """Generate redirections.""" name = "redirect" def gen_tasks(self): """Generate redirections tasks.""" - kw = { 'redirections': self.site.config['REDIRECTIONS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin index b4b43a3..72ce31f 100644 --- a/nikola/plugins/task/robots.plugin +++ b/nikola/plugins/task/robots.plugin @@ -1,10 +1,13 @@ [Core] -Name = robots -Module = robots +name = robots +module = robots [Documentation] -Author = Daniel Aleksandersen -Version = 1.0 -Website = http://getnikola.com -Description = Generate /robots.txt exclusion file and promote sitemap. +author = Daniel Aleksandersen +version = 1.0 +website = http://getnikola.com +description = Generate /robots.txt exclusion file and promote sitemap. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py index 2f25a21..65254b6 100644 --- a/nikola/plugins/task/robots.py +++ b/nikola/plugins/task/robots.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Generate a robots.txt file.""" + from __future__ import print_function, absolute_import, unicode_literals import io import os @@ -37,12 +39,13 @@ from nikola import utils class RobotsFile(LateTask): - """Generate a robots.txt.""" + + """Generate a robots.txt file.""" name = "robots_file" def gen_tasks(self): - """Generate a robots.txt.""" + """Generate a robots.txt file.""" kw = { "base_url": self.site.config["BASE_URL"], "site_url": self.site.config["SITE_URL"], diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin index 56f0bf4..cf9b7a7 100644 --- a/nikola/plugins/task/rss.plugin +++ b/nikola/plugins/task/rss.plugin @@ -1,10 +1,13 @@ [Core] -Name = generate_rss -Module = rss +name = generate_rss +module = rss [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Generate RSS feeds. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Generate RSS feeds. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py index 26a4da1..9020a06 100644 --- a/nikola/plugins/task/rss.py +++ b/nikola/plugins/task/rss.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Generate RSS feeds.""" + from __future__ import unicode_literals, print_function import os try: @@ -36,11 +38,13 @@ from nikola.plugin_categories import Task class GenerateRSS(Task): + """Generate RSS feeds.""" name = "generate_rss" def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('rss', self.rss_path) return super(GenerateRSS, self).set_site(site) @@ -102,5 +106,6 @@ class GenerateRSS(Task): yield utils.apply_filters(task, kw['filters']) def rss_path(self, name, lang): + """Return RSS path.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['RSS_PATH'], 'rss.xml'] if _f] diff --git a/nikola/plugins/task/scale_images.plugin b/nikola/plugins/task/scale_images.plugin index c0f0f28..d906b8c 100644 --- a/nikola/plugins/task/scale_images.plugin +++ b/nikola/plugins/task/scale_images.plugin @@ -1,9 +1,13 @@ [Core] -Name = scale_images -Module = scale_images +name = scale_images +module = scale_images [Documentation] -Author = Pelle Nilsson -Version = 1.0 -Website = http://getnikola.com -Description = Create down-scaled images and thumbnails. +author = Pelle Nilsson +version = 1.0 +website = http://getnikola.com +description = Create down-scaled images and thumbnails. + +[Nikola] +plugincategory = Task + diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py index f97027e..22ed2ab 100644 --- a/nikola/plugins/task/scale_images.py +++ b/nikola/plugins/task/scale_images.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Resize images and create thumbnails for them.""" + import os from nikola.plugin_categories import Task @@ -32,17 +34,18 @@ from nikola import utils class ScaleImage(Task, ImageProcessor): - """Copy static files into the output folder.""" + + """Resize images and create thumbnails for them.""" name = "scale_images" def set_site(self, site): - self.logger = utils.get_logger('scale_images', site.loghandlers) + """Set Nikola site.""" + self.logger = utils.get_logger('scale_images', utils.STDERR_HANDLER) return super(ScaleImage, self).set_site(site) def process_tree(self, src, dst): - """Processes all images in a src tree and put the (possibly) rescaled - images in the dst folder.""" + """Process all images in a src tree and put the (possibly) rescaled images in the dst folder.""" ignore = set(['.svn']) base_len = len(src.split(os.sep)) for root, dirs, files in os.walk(src, followlinks=True): @@ -68,12 +71,12 @@ class ScaleImage(Task, ImageProcessor): } def process_image(self, src, dst, thumb): + """Resize an image.""" self.resize_image(src, dst, self.kw['max_image_size'], False) self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False) def gen_tasks(self): """Copy static files into the output folder.""" - self.kw = { 'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'], 'max_image_size': self.site.config['MAX_IMAGE_SIZE'], diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin index 0b992b8..e3c991f 100644 --- a/nikola/plugins/task/sitemap.plugin +++ b/nikola/plugins/task/sitemap.plugin @@ -1,10 +1,13 @@ [Core] -Name = sitemap -Module = sitemap +name = sitemap +module = sitemap [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Generate google sitemap. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Generate google sitemap. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py index 92d557d..fd781d6 100644 --- a/nikola/plugins/task/sitemap/__init__.py +++ b/nikola/plugins/task/sitemap/__init__.py @@ -24,9 +24,12 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Generate a sitemap.""" + from __future__ import print_function, absolute_import, unicode_literals import io import datetime +import dateutil.tz import os try: from urlparse import urljoin, urlparse @@ -42,6 +45,7 @@ from nikola.utils import config_changed, apply_filters urlset_header = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:xhtml="http://www.w3.org/1999/xhtml" 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"> @@ -58,6 +62,7 @@ urlset_footer = "</urlset>" sitemapindex_header = """<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:xhtml="http://www.w3.org/1999/xhtml" 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"> @@ -76,7 +81,7 @@ sitemapindex_footer = "</sitemapindex>" def get_base_path(base): - """returns the path of a base URL if it contains one. + """Return the path of a base URL if it contains one. >>> get_base_path('http://some.site') == '/' True @@ -101,6 +106,7 @@ def get_base_path(base): class Sitemap(LateTask): + """Generate a sitemap.""" name = "sitemap" @@ -114,10 +120,12 @@ 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', ['.atom', '.html', '.htm', '.xml', '.rss']), + "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.php', '.xml', '.rss']), "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"], "filters": self.site.config["FILTERS"], "translations": self.site.config["TRANSLATIONS"], + "tzinfo": self.site.config['__tzinfo__'], + "sitemap_plugin_revision": 1, } output = kw['output_folder'] @@ -132,6 +140,7 @@ class Sitemap(LateTask): urlset = {} def scan_locs(): + """Scan site locations.""" 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 @@ -169,17 +178,18 @@ class Sitemap(LateTask): filehead = fh.read(1024) fh.close() - if path.endswith('.html') or path.endswith('.htm'): + if path.endswith('.html') or path.endswith('.htm') or path.endswith('.php'): """ ignores "html" files without doctype """ if b'<!doctype html' not in filehead.lower(): continue """ ignores "html" files with noindex robot directives """ - robots_directives = [b'<meta content="noindex" name="robots"', - b'<meta content="none" name="robots"', - b'<meta name="robots" content="noindex"', - b'<meta name="robots" content="none"'] - if any([robot_directive in filehead.lower() for robot_directive in robots_directives]): + robots_directives = [b'<meta content=noindex name=robots', + b'<meta content=none name=robots', + b'<meta name=robots content=noindex', + b'<meta name=robots content=none'] + lowquothead = filehead.lower().decode('utf-8', 'ignore').replace('"', '').encode('utf-8') + if any([robot_directive in lowquothead for robot_directive in robots_directives]): continue # put Atom and RSS in sitemapindex[] instead of in urlset[], @@ -210,6 +220,7 @@ class Sitemap(LateTask): urlset[loc] = loc_format.format(loc, lastmod, '\n'.join(alternates)) def robot_fetch(path): + """Check if robots can fetch a file.""" for rule in kw["robots_exclusions"]: robot = robotparser.RobotFileParser() robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)]) @@ -218,6 +229,7 @@ class Sitemap(LateTask): return True def write_sitemap(): + """Write sitemap to file.""" # Have to rescan, because files may have been added between # task dep scanning and task execution with io.open(sitemap_path, 'w+', encoding='utf8') as outf: @@ -229,16 +241,19 @@ class Sitemap(LateTask): sitemapindex[sitemap_url] = sitemap_format.format(sitemap_url, self.get_lastmod(sitemap_path)) def write_sitemapindex(): + """Write sitemap index.""" with io.open(sitemapindex_path, 'w+', encoding='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(): + """Yield a task to calculate the dependencies of the sitemap. + + Other tasks can depend on this output, instead of having + to scan locations. + """ scan_locs() # Generate a list of file dependencies for the actual generation @@ -290,10 +305,15 @@ class Sitemap(LateTask): }, kw['filters']) def get_lastmod(self, p): + """Get last modification date.""" if self.site.invariant: return '2038-01-01' else: - return datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0] + # RFC 3339 (web ISO 8601 profile) represented in UTC with Zulu + # zone desgignator as recommeded for sitemaps. Second and + # microsecond precision is stripped for compatibility. + lastmod = datetime.datetime.utcfromtimestamp(os.stat(p).st_mtime).replace(tzinfo=dateutil.tz.gettz('UTC'), second=0, microsecond=0).isoformat().replace('+00:00', 'Z') + return lastmod if __name__ == '__main__': import doctest diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin index 5560df6..d232c2b 100644 --- a/nikola/plugins/task/sources.plugin +++ b/nikola/plugins/task/sources.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_sources -Module = sources +name = render_sources +module = sources [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Copy page sources into the output. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Copy page sources into the output. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py index 840a31c..87b4ae7 100644 --- a/nikola/plugins/task/sources.py +++ b/nikola/plugins/task/sources.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Copy page sources into the output.""" + import os from nikola.plugin_categories import Task @@ -31,20 +33,13 @@ from nikola import utils class Sources(Task): + """Copy page sources into the output.""" name = "render_sources" def gen_tasks(self): - """Publish the page sources into the output. - - Required keyword arguments: - - translations - default_lang - post_pages - output_folder - """ + """Publish the page sources into the output.""" kw = { "translations": self.site.config["TRANSLATIONS"], "output_folder": self.site.config["OUTPUT_FOLDER"], diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin index 4ac3800..283a16a 100644 --- a/nikola/plugins/task/tags.plugin +++ b/nikola/plugins/task/tags.plugin @@ -1,10 +1,13 @@ [Core] -Name = render_tags -Module = tags +name = render_tags +module = tags [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Render the tag pages and feeds. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Render the tag pages and feeds. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py index 832ceff..3186636 100644 --- a/nikola/plugins/task/tags.py +++ b/nikola/plugins/task/tags.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Render the tag/category pages and feeds.""" + from __future__ import unicode_literals import json import os @@ -39,11 +41,13 @@ from nikola import utils class RenderTags(Task): + """Render the tag/category pages and feeds.""" name = "render_tags" def set_site(self, site): + """Set Nikola site.""" site.register_path_handler('tag_index', self.tag_index_path) site.register_path_handler('category_index', self.category_index_path) site.register_path_handler('tag', self.tag_path) @@ -56,7 +60,6 @@ class RenderTags(Task): def gen_tasks(self): """Render the tag pages and feeds.""" - kw = { "translations": self.site.config["TRANSLATIONS"], "blog_title": self.site.config["BLOG_TITLE"], @@ -121,6 +124,7 @@ class RenderTags(Task): cat_list = list(self.site.posts_per_category.items()) def render_lists(tag, posts, is_category=True): + """Render tag pages as RSS files and lists/indexes.""" post_list = sorted(posts, key=lambda a: a.date) post_list.reverse() for lang in kw["translations"]: @@ -161,6 +165,7 @@ class RenderTags(Task): 'assets', 'js', 'tag_cloud_data.json') def write_tag_data(data): + """Write tag data into JSON file, for use in tag clouds.""" utils.makedirs(os.path.dirname(output_name)) with open(output_name, 'w+') as fd: json.dump(data, fd) @@ -178,20 +183,20 @@ class RenderTags(Task): yield utils.apply_filters(task, kw['filters']) def _create_tags_page(self, kw, include_tags=True, include_categories=True): - """a global "all your tags/categories" page for each language""" - tags = natsort.natsorted([tag for tag in self.site.posts_per_tag.keys() - if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]], - alg=natsort.ns.F | natsort.ns.IC) + """Create a global "all your tags/categories" page for each language.""" categories = [cat.category_name for cat in self.site.category_hierarchy] - has_tags = (tags != []) and include_tags has_categories = (categories != []) and include_categories template_name = "tags.tmpl" kw = kw.copy() - if include_tags: - kw['tags'] = tags if include_categories: kw['categories'] = categories for lang in kw["translations"]: + tags = natsort.natsorted([tag for tag in self.site.tags_per_language[lang] + if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]], + alg=natsort.ns.F | natsort.ns.IC) + has_tags = (tags != []) and include_tags + if include_tags: + kw['tags'] = tags output_name = os.path.join( kw['output_folder'], self.site.path('tag_index' if has_tags else 'category_index', None, lang)) output_name = output_name @@ -219,6 +224,7 @@ class RenderTags(Task): context["cat_items"] = None context["permalink"] = self.site.link("tag_index" if has_tags else "category_index", None, lang) context["description"] = context["title"] + context["pagekind"] = ["list", "tags_page"] task = self.site.generic_post_list_renderer( lang, [], @@ -232,7 +238,7 @@ class RenderTags(Task): yield task def list_tags_page(self, kw): - """a global "all your tags/categories" page for each language""" + """Create a global "all your tags/categories" page for each language.""" if self.site.config['TAG_PATH'] == self.site.config['CATEGORY_PATH']: yield self._create_tags_page(kw, True, True) else: @@ -254,9 +260,7 @@ class RenderTags(Task): return [(child.name, self.site.link("category", child.category_name)) for child in node.children] def tag_page_as_index(self, tag, lang, post_list, kw, is_category): - """render a sort of index page collection using only this - tag's posts.""" - + """Render a sort of index page collection using only this tag's posts.""" kind = "category" if is_category else "tag" def page_link(i, displayed_i, num_pages, force_addition, extension=None): @@ -284,12 +288,13 @@ class RenderTags(Task): context_source["description"] = self._get_description(tag, is_category, lang) if is_category: context_source["subcategories"] = self._get_subcategories(tag) + context_source["pagekind"] = ["index", "tag_page"] template_name = "tagindex.tmpl" yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path) def tag_page_as_list(self, tag, lang, post_list, kw, is_category): - """We render a single flat link list with this tag's posts""" + """Render a single flat link list with this tag's posts.""" kind = "category" if is_category else "tag" template_name = "tag.tmpl" output_name = os.path.join(kw['output_folder'], self.site.path( @@ -308,6 +313,7 @@ class RenderTags(Task): context["description"] = self._get_description(tag, is_category, lang) if is_category: context["subcategories"] = self._get_subcategories(tag) + context["pagekind"] = ["list", "tag_page"] task = self.site.generic_post_list_renderer( lang, post_list, @@ -321,7 +327,7 @@ class RenderTags(Task): yield task def tag_rss(self, tag, lang, posts, kw, is_category): - """RSS for a single tag / language""" + """Create a RSS feed for a single tag in a given language.""" kind = "category" if is_category else "tag" # Render RSS output_name = os.path.normpath( @@ -352,21 +358,25 @@ class RenderTags(Task): return utils.apply_filters(task, kw['filters']) def slugify_tag_name(self, name): + """Slugify a tag name.""" if self.site.config['SLUG_TAG_PATH']: name = utils.slugify(name) return name def tag_index_path(self, name, lang): + """Return path to the tag index.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['TAG_PATH'], self.site.config['INDEX_FILE']] if _f] def category_index_path(self, name, lang): + """Return path to the category index.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['CATEGORY_PATH'], self.site.config['INDEX_FILE']] if _f] def tag_path(self, name, lang): + """Return path to a tag.""" if self.site.config['PRETTY_URLS']: return [_f for _f in [ self.site.config['TRANSLATIONS'][lang], @@ -380,16 +390,19 @@ class RenderTags(Task): self.slugify_tag_name(name) + ".html"] if _f] def tag_atom_path(self, name, lang): + """Return path to a tag Atom feed.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".atom"] if _f] def tag_rss_path(self, name, lang): + """Return path to a tag RSS feed.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".xml"] if _f] def slugify_category_name(self, name): + """Slugify a category name.""" path = self.site.parse_category_name(name) if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']: path = path[-1:] # only the leaf @@ -404,6 +417,7 @@ class RenderTags(Task): return path def category_path(self, name, lang): + """Return path to a category.""" if self.site.config['PRETTY_URLS']: return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['CATEGORY_PATH']] if @@ -414,11 +428,13 @@ class RenderTags(Task): _f] + self._add_extension(self.slugify_category_name(name), ".html") def category_atom_path(self, name, lang): + """Return path to a category Atom feed.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['CATEGORY_PATH']] if _f] + self._add_extension(self.slugify_category_name(name), ".atom") def category_rss_path(self, name, lang): + """Return path to a category RSS feed.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], self.site.config['CATEGORY_PATH']] if _f] + self._add_extension(self.slugify_category_name(name), ".xml") diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py index a1d17a6..d416ad7 100644 --- a/nikola/plugins/template/__init__.py +++ b/nikola/plugins/template/__init__.py @@ -23,3 +23,5 @@ # 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. + +"""Default template engines for Nikola.""" diff --git a/nikola/plugins/template/jinja.plugin b/nikola/plugins/template/jinja.plugin index 0bdcb94..cfe9fa8 100644 --- a/nikola/plugins/template/jinja.plugin +++ b/nikola/plugins/template/jinja.plugin @@ -1,9 +1,13 @@ [Core] -Name = jinja -Module = jinja +name = jinja +module = jinja [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Support for Jinja2 templates. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Support for Jinja2 templates. + +[Nikola] +plugincategory = Template + diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py index 82e8397..b02d75c 100644 --- a/nikola/plugins/template/jinja.py +++ b/nikola/plugins/template/jinja.py @@ -24,8 +24,10 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Jinja template handlers""" +"""Jinja template handler.""" + +from __future__ import unicode_literals import os import json from collections import deque @@ -40,14 +42,15 @@ from nikola.utils import makedirs, req_missing class JinjaTemplates(TemplateSystem): - """Wrapper for Jinja2 templates.""" + + """Support for Jinja2 templates.""" name = "jinja" lookup = None dependency_cache = {} def __init__(self): - """ initialize Jinja2 wrapper with extended set of filters""" + """Initialize Jinja2 environment with extended set of filters.""" if jinja2 is None: return self.lookup = jinja2.Environment() @@ -59,26 +62,25 @@ class JinjaTemplates(TemplateSystem): self.lookup.globals['tuple'] = tuple def set_directories(self, directories, cache_folder): - """Create a template lookup.""" + """Create a new template lookup with set directories.""" if jinja2 is None: req_missing(['jinja2'], 'use this theme') 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.""" + """Add a directory to the lookup and recreate it if it's not there yet.""" if directory not in self.directories: self.directories.append(directory) self.create_lookup() def create_lookup(self): - """Create a template lookup object.""" + """Create a template lookup.""" self.lookup.loader = jinja2.FileSystemLoader(self.directories, encoding='utf-8') def set_site(self, site): - """Sets the site.""" + """Set the Nikola site.""" self.site = site self.lookup.filters.update(self.site.config['TEMPLATE_FILTERS']) @@ -99,6 +101,7 @@ class JinjaTemplates(TemplateSystem): return self.lookup.from_string(template).render(**context) def template_deps(self, template_name): + """Generate list of dependencies for a template.""" # Cache the lists of dependencies for each template name. if self.dependency_cache.get(template_name) is None: # Use a breadth-first search to find all templates this one diff --git a/nikola/plugins/template/mako.plugin b/nikola/plugins/template/mako.plugin index 2fe6d98..d256faf 100644 --- a/nikola/plugins/template/mako.plugin +++ b/nikola/plugins/template/mako.plugin @@ -1,9 +1,13 @@ [Core] -Name = mako -Module = mako +name = mako +module = mako [Documentation] -Author = Roberto Alsina -Version = 1.0 -Website = http://getnikola.com -Description = Support for Mako templates. +author = Roberto Alsina +version = 1.0 +website = http://getnikola.com +description = Support for Mako templates. + +[Nikola] +plugincategory = Template + diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py index e5545f6..aed6596 100644 --- a/nikola/plugins/template/mako.py +++ b/nikola/plugins/template/mako.py @@ -24,14 +24,15 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Mako template handlers""" +"""Mako template handler.""" + from __future__ import unicode_literals, print_function, absolute_import import os import shutil import sys import tempfile -from mako import util, lexer +from mako import util, lexer, parsetree from mako.lookup import TemplateLookup from mako.template import Template from markupsafe import Markup # It's ok, Mako requires it @@ -43,7 +44,8 @@ LOGGER = get_logger('mako', STDERR_HANDLER) class MakoTemplates(TemplateSystem): - """Wrapper for Mako templates.""" + + """Support for Mako templates.""" name = "mako" @@ -54,6 +56,7 @@ class MakoTemplates(TemplateSystem): cache_dir = None def get_deps(self, filename): + """Get dependencies for a template (internal function).""" text = util.read_file(filename) lex = lexer.Lexer(text=text, filename=filename) lex.parse() @@ -61,13 +64,12 @@ class MakoTemplates(TemplateSystem): deps = [] for n in lex.template.nodes: keyword = getattr(n, 'keyword', None) - if keyword in ["inherit", "namespace"]: + if keyword in ["inherit", "namespace"] or isinstance(n, parsetree.IncludeTag): deps.append(n.attributes['file']) - # TODO: include tags are not handled return deps def set_directories(self, directories, cache_folder): - """Set directories and create a template lookup.""" + """Create a new template lookup with set directories.""" cache_dir = os.path.join(cache_folder, '.mako.tmp') # Workaround for a Mako bug, Issue #825 if sys.version_info[0] == 2: @@ -83,21 +85,20 @@ class MakoTemplates(TemplateSystem): 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.""" + """Add a directory to the lookup and recreate it if it's not there yet.""" if directory not in self.directories: self.directories.append(directory) self.create_lookup() def create_lookup(self): - """Create a template lookup object.""" + """Create a template lookup.""" self.lookup = TemplateLookup( directories=self.directories, module_directory=self.cache_dir, output_encoding='utf-8') def set_site(self, site): - """Sets the site.""" + """Set the Nikola site.""" self.site = site self.filters.update(self.site.config['TEMPLATE_FILTERS']) @@ -113,14 +114,12 @@ class MakoTemplates(TemplateSystem): return data def render_template_to_string(self, template, context): - """ Render template to a string using context. """ - + """Render template to a string using context.""" context.update(self.filters) - return Template(template).render(**context) def template_deps(self, template_name): - """Returns filenames which are dependencies for a template.""" + """Generate list of dependencies for a template.""" # We can cache here because dependencies should # not change between runs if self.cache.get(template_name, None) is None: @@ -134,4 +133,5 @@ class MakoTemplates(TemplateSystem): def striphtml(text): + """Strip HTML tags from text.""" return Markup(text).striptags() diff --git a/nikola/post.py b/nikola/post.py index 466d5e0..7badfc6 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The Post class.""" + from __future__ import unicode_literals, print_function, absolute_import import io @@ -68,7 +70,7 @@ from .utils import ( ) from .rc4 import rc4 -__all__ = ['Post'] +__all__ = ('Post',) TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE) _UPGRADE_METADATA_ADVERTISED = False @@ -76,7 +78,7 @@ _UPGRADE_METADATA_ADVERTISED = False class Post(object): - """Represents a blog post or web page.""" + """Represent a blog post or site page.""" def __init__( self, @@ -102,7 +104,7 @@ class Post(object): if self.config['FUTURE_IS_NOW']: self.current_time = None else: - self.current_time = current_time() + self.current_time = current_time(tzinfo) self.translated_to = set([]) self._prev_post = None self._next_post = None @@ -231,6 +233,7 @@ class Post(object): self.compiler.register_extra_dependencies(self) def __repr__(self): + """Provide a representation of the post object.""" # Calculate a hash that represents most data about the post m = hashlib.md5() # source_path modification date (to avoid reading it) @@ -255,24 +258,32 @@ class Post(object): @property def alltags(self): - """This is ALL the tags for this post.""" + """Return ALL the tags for this post.""" tags = [] for l in self._tags: tags.extend(self._tags[l]) return list(set(tags)) - @property - def tags(self): - lang = nikola.utils.LocaleBorg().current_lang + def tags_for_language(self, lang): + """Return tags for a given language.""" if lang in self._tags: return self._tags[lang] + elif lang not in self.translated_to and self.skip_untranslated: + return [] elif self.default_lang in self._tags: return self._tags[self.default_lang] else: return [] @property + def tags(self): + """Return tags for the current language.""" + lang = nikola.utils.LocaleBorg().current_lang + return self.tags_for_language(lang) + + @property def prev_post(self): + """Return previous post.""" lang = nikola.utils.LocaleBorg().current_lang rv = self._prev_post while self.skip_untranslated: @@ -285,10 +296,12 @@ class Post(object): @prev_post.setter # NOQA def prev_post(self, v): + """Set previous post.""" self._prev_post = v @property def next_post(self): + """Return next post.""" lang = nikola.utils.LocaleBorg().current_lang rv = self._next_post while self.skip_untranslated: @@ -301,24 +314,32 @@ class Post(object): @next_post.setter # NOQA def next_post(self, v): + """Set next post.""" self._next_post = v @property def template_name(self): + """Return template name for this post.""" return self.meta('template') or self._template_name def formatted_date(self, date_format, date=None): - """Return the formatted date, as unicode.""" - if date: - fmt_date = date.strftime(date_format) + """Return the formatted date as unicode.""" + date = date if date else self.date + + if date_format == 'webiso': + # Formatted after RFC 3339 (web ISO 8501 profile) with Zulu + # zone desgignator for times in UTC and no microsecond precision. + fmt_date = date.replace(microsecond=0).isoformat().replace('+00:00', 'Z') else: - fmt_date = self.date.strftime(date_format) + fmt_date = date.strftime(date_format) + # Issue #383, this changes from py2 to py3 if isinstance(fmt_date, bytes_str): fmt_date = fmt_date.decode('utf8') return fmt_date def formatted_updated(self, date_format): + """Return the updated date as unicode.""" return self.formatted_date(date_format, self.updated) def title(self, lang=None): @@ -353,7 +374,7 @@ class Post(object): return self.meta[lang]['description'] def add_dependency(self, dependency, add='both', lang=None): - """Adds a file dependency for tasks using that post. + """Add a file dependency for tasks using that post. The ``dependency`` should be a string specifying a path, or a callable which returns such a string or a list of strings. @@ -365,7 +386,8 @@ class Post(object): includes the HTML resulting from compiling the fragment ('page' or 'both'). - If ``lang`` is not specified, this dependency is added for all languages.""" + If ``lang`` is not specified, this dependency is added for all languages. + """ if add not in {'fragment', 'page', 'both'}: raise Exception("Add parameter is '{0}', but must be either 'fragment', 'page', or 'both'.".format(add)) if add == 'fragment' or add == 'both': @@ -374,7 +396,7 @@ class Post(object): self._dependency_file_page[lang].append((type(dependency) != str, dependency)) def add_dependency_uptodate(self, dependency, is_callable=False, add='both', lang=None): - """Adds a dependency for task's ``uptodate`` for tasks using that post. + """Add a dependency for task's ``uptodate`` for tasks using that post. This can be for example an ``utils.config_changed`` object, or a list of such objects. @@ -397,7 +419,6 @@ class Post(object): post.add_dependency_uptodate( utils.config_changed({1: some_data}, 'uniqueid'), False, 'page') - """ if add == 'fragment' or add == 'both': self._dependency_uptodate_fragment[lang].append((is_callable, dependency)) @@ -433,13 +454,14 @@ class Post(object): deps.extend([cand_1, cand_2]) deps += self._get_dependencies(self._dependency_file_page[lang]) deps += self._get_dependencies(self._dependency_file_page[None]) - return deps + return sorted(deps) def deps_uptodate(self, lang): """Return a list of uptodate dependencies to build this post's page. These dependencies should be included in ``uptodate`` for the task - which generates the page.""" + which generates the page. + """ deps = [] deps += self._get_dependencies(self._dependency_uptodate_page[lang]) deps += self._get_dependencies(self._dependency_uptodate_page[None]) @@ -448,7 +470,6 @@ class Post(object): def compile(self, lang): """Generate the cache/ file with the compiled post.""" - def wrap_encrypt(path, password): """Wrap a post with encryption.""" with io.open(path, 'r+', encoding='utf8') as inf: @@ -480,7 +501,8 @@ class Post(object): """Return a list of uptodate dependencies to build this post's fragment. These dependencies should be included in ``uptodate`` for the task - which generates the fragment.""" + which generates the fragment. + """ deps = [] if self.default_lang in self.translated_to: deps.append(self.source_path) @@ -493,7 +515,7 @@ class Post(object): deps = [d for d in deps if os.path.exists(d)] deps += self._get_dependencies(self._dependency_file_fragment[lang]) deps += self._get_dependencies(self._dependency_file_fragment[None]) - return deps + return sorted(deps) def fragment_deps_uptodate(self, lang): """Return a list of file dependencies to build this post's fragment.""" @@ -504,7 +526,7 @@ class Post(object): return deps def is_translation_available(self, lang): - """Return true if the translation actually exists.""" + """Return True if the translation actually exists.""" return lang in self.translated_to def translated_source_path(self, lang): @@ -548,7 +570,6 @@ class Post(object): All links in the returned HTML will be relative. The HTML returned is a bare fragment, not a full document. """ - if lang is None: lang = nikola.utils.LocaleBorg().current_lang file_name = self._translated_file_path(lang) @@ -584,23 +605,23 @@ class Post(object): data = lxml.html.tostring(document, encoding='unicode') if teaser_only: - teaser = TEASER_REGEXP.split(data)[0] + teaser_regexp = self.config.get('TEASER_REGEXP', TEASER_REGEXP) + teaser = teaser_regexp.split(data)[0] if teaser != data: 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), - TEASER_REGEXP.search(data).groups()[-1]) + if teaser_regexp.search(data).groups()[-1]: + teaser_text = teaser_regexp.search(data).groups()[-1] else: - 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, query=rss_links_append_query), - 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) + teaser_text = 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, query=rss_links_append_query), + read_more=teaser_text, + 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) try: @@ -720,6 +741,7 @@ class Post(object): return path def permalink(self, lang=None, absolute=False, extension='.html', query=None): + """Return permalink for a post.""" if lang is None: lang = nikola.utils.LocaleBorg().current_lang @@ -746,6 +768,7 @@ class Post(object): @property def previewimage(self, lang=None): + """Return the previewimage path.""" if lang is None: lang = nikola.utils.LocaleBorg().current_lang @@ -759,13 +782,11 @@ class Post(object): return image_path def source_ext(self, prefix=False): - """ - Return the source file extension. + """Return the source file extension. If `prefix` is True, a `.src.` prefix will be added to the resulting extension - if it’s equal to the destination extension. + if it's equal to the destination extension. """ - ext = os.path.splitext(self.source_path)[1] # do not publish PHP sources if prefix and ext == '.html': @@ -778,7 +799,7 @@ class Post(object): def re_meta(line, match=None): - """re.compile for meta""" + """Find metadata using regular expressions.""" if match: reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match))) else: @@ -793,10 +814,9 @@ def re_meta(line, match=None): 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. + """Try to reed the metadata from the filename based on the given re. + This requires to use symbolic group names in the pattern. The part to read the metadata from the filename based on a regular expression is taken from Pelican - pelican/readers.py """ @@ -816,7 +836,7 @@ def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_ti def get_metadata_from_file(source_path, config=None, lang=None): - """Extracts metadata from the file itself, by parsing contents.""" + """Extract metadata from the file itself, by parsing contents.""" try: if lang and config: source_path = get_translation_candidate(config, source_path, lang) @@ -832,26 +852,10 @@ def get_metadata_from_file(source_path, config=None, lang=None): def _get_metadata_from_file(meta_data): - """Parse file contents and obtain metadata. - - >>> g = _get_metadata_from_file - >>> list(g([]).values()) - [] - >>> str(g(["======","FooBar","======"])["title"]) - 'FooBar' - >>> str(g(["FooBar","======"])["title"]) - 'FooBar' - >>> str(g(["#FooBar"])["title"]) - 'FooBar' - >>> str(g([".. title: FooBar"])["title"]) - 'FooBar' - >>> 'title' in g(["","",".. title: FooBar"]) - False - >>> 'title' in g(["",".. title: FooBar"]) # for #520 - True - - """ + """Extract metadata from a post's source file.""" meta = {} + if not meta_data: + return meta re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#'))) # Assuming rst titles are going to be at least 4 chars long @@ -859,37 +863,40 @@ def _get_metadata_from_file(meta_data): re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape( string.punctuation))) + # Skip up to one empty line at the beginning (for txt2tags) + if not meta_data[0]: + meta_data = meta_data[1:] + + # First, get metadata from the beginning of the file, + # up to first empty line + for i, line in enumerate(meta_data): - # txt2tags requires an empty line at the beginning - # and since we are here because it's a 1-file post - # let's be flexible on what we accept, so, skip empty - # first lines. - if not line and i > 0: + if not line: break - if 'title' not in meta: - match = re_meta(line, 'title') - if match[0]: - meta['title'] = match[1] - if 'title' not in meta: + match = re_meta(line) + if match[0]: + meta[match[0]] = match[1] + + # If we have no title, try to get it from document + if 'title' not in meta: + piece = meta_data[:] + for i, line in enumerate(piece): if re_rst_title.findall(line) and i > 0: meta['title'] = meta_data[i - 1].strip() - if 'title' not in meta: + break if (re_rst_title.findall(line) and i >= 0 and re_rst_title.findall(meta_data[i + 2])): meta['title'] = meta_data[i + 1].strip() - if 'title' not in meta: + break if re_md_title.findall(line): meta['title'] = re_md_title.findall(line)[0] - - match = re_meta(line) - if match[0]: - meta[match[0]] = match[1] + break return meta def get_metadata_from_meta_file(path, config=None, lang=None): - """Takes a post path, and gets data from a matching .meta file.""" + """Take a post path, and gets data from a matching .meta file.""" global _UPGRADE_METADATA_ADVERTISED meta_path = os.path.splitext(path)[0] + '.meta' if lang and config: @@ -977,12 +984,15 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None) file_metadata_regexp, unslugify_titles)) + compiler_meta = {} + if getattr(post, 'compiler', None): compiler_meta = post.compiler.read_metadata(post, file_metadata_regexp, unslugify_titles, lang) meta.update(compiler_meta) - if not post.is_two_file: + if not post.is_two_file and not compiler_meta: # Meta file has precedence over file, which can contain garbage. + # Moreover, we should not to talk to the file if we have compiler meta. meta.update(get_metadata_from_file(post.source_path, config, lang)) if lang is None: @@ -1002,6 +1012,7 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None) def hyphenate(dom, _lang): + """Hyphenate a post.""" # circular import prevention from .nikola import LEGAL_VALUES lang = LEGAL_VALUES['PYPHEN_LOCALES'].get(_lang, pyphen.language_fallback(_lang)) @@ -1029,6 +1040,7 @@ def hyphenate(dom, _lang): def insert_hyphens(node, hyphenator): + """Insert hyphens into a node.""" textattrs = ('text', 'tail') if isinstance(node, lxml.etree._Entity): # HTML entities have no .text diff --git a/nikola/rc4.py b/nikola/rc4.py index b46d602..93b660f 100644 --- a/nikola/rc4.py +++ b/nikola/rc4.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """ - A RC4 encryption library (used for password-protected posts) - --- +A RC4 encryption library (used for password-protected posts). + +Original RC4 code license: + Copyright (C) 2012 Bo Zhu http://about.bozhu.me Permission is hereby granted, free of charge, to any person obtaining a @@ -28,6 +30,7 @@ import sys def KSA(key): + """Run Key Scheduling Algorithm.""" keylength = len(key) S = list(range(256)) @@ -41,6 +44,7 @@ def KSA(key): def PRGA(S): + """Run Pseudo-Random Generation Algorithm.""" i = 0 j = 0 while True: @@ -53,16 +57,17 @@ def PRGA(S): def RC4(key): + """Generate RC4 keystream.""" S = KSA(key) return PRGA(S) def rc4(key, string): """Encrypt things. + >>> print(rc4("Key", "Plaintext")) u/MW6NlArwrT """ - string.encode('utf8') key.encode('utf8') diff --git a/nikola/utils.py b/nikola/utils.py index 3708775..3a268ff 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -47,6 +47,7 @@ import logbook import warnings import PyRSS2Gen as rss from collections import defaultdict, Callable +from logbook.compat import redirect_logging from logbook.more import ExceptionHandler, ColorizedStderrHandler from pygments.formatters import HtmlFormatter from zipfile import ZipFile as zipf @@ -57,7 +58,7 @@ from doit.cmdparse import CmdParse from nikola import DEBUG -__all__ = ['CustomEncoder', 'get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree', +__all__ = ('CustomEncoder', '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', @@ -69,7 +70,7 @@ __all__ = ['CustomEncoder', 'get_theme_path', 'get_theme_chain', 'load_messages' 'adjust_name_for_index_path', 'adjust_name_for_index_link', 'NikolaPygmentsHTML', 'create_redirect', 'TreeNode', 'flatten_tree_structure', 'parse_escaped_hierarchical_category_name', - 'join_hierarchical_category_path', 'indent'] + 'join_hierarchical_category_path', 'indent') # Are you looking for 'generic_rss_renderer'? # It's defined in nikola.nikola.Nikola (the site object). @@ -93,7 +94,9 @@ class ApplicationWarning(Exception): class ColorfulStderrHandler(ColorizedStderrHandler): + """Stream handler with colors.""" + _colorful = False def should_colorize(self, record): @@ -116,14 +119,14 @@ STDERR_HANDLER = [ColorfulStderrHandler( 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') USE_SLUGIFY = True -# This will block out the default handler and will hide all unwanted -# messages, properly. -logbook.NullHandler().push_application() +redirect_logging() if DEBUG: logging.basicConfig(level=logging.DEBUG) @@ -132,7 +135,7 @@ else: def showwarning(message, category, filename, lineno, file=None, line=None): - """Show a warning (from the warnings subsystem) to the user.""" + """Show a warning (from the warnings module) to the user.""" try: n = category.__name__ except AttributeError: @@ -200,14 +203,14 @@ def sys_encode(thing): def sys_decode(thing): - """Returns unicode.""" + """Return Unicode.""" if isinstance(thing, bytes_str): return thing.decode(ENCODING) return thing def makedirs(path): - """Create a folder.""" + """Create a folder and its parents if needed (mkdir -p).""" if not path: return if os.path.exists(path): @@ -229,13 +232,12 @@ class Functionary(defaultdict): """Class that looks like a function, but is a defaultdict.""" def __init__(self, default, default_lang): + """Initialize a functionary.""" super(Functionary, self).__init__(default) self.default_lang = default_lang def __call__(self, key, lang=None): - """When called as a function, take an optional lang - and return self[lang][key].""" - + """When called as a function, take an optional lang and return self[lang][key].""" if lang is None: lang = LocaleBorg().current_lang return self[lang][key] @@ -243,8 +245,7 @@ class Functionary(defaultdict): class TranslatableSetting(object): - """ - A setting that can be translated. + """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, @@ -258,7 +259,6 @@ class TranslatableSetting(object): 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 @@ -276,6 +276,7 @@ class TranslatableSetting(object): return self().__getattribute__(attr) def __dir__(self): + """Return the available methods of TranslatableSettings and strings.""" return list(set(self.__dict__).union(set(dir(str)))) def __init__(self, name, inp, translations): @@ -286,7 +287,6 @@ class TranslatableSetting(object): * 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 @@ -322,8 +322,7 @@ class TranslatableSetting(object): return self.default_lang def __call__(self, lang=None): - """ - Return the value in the requested language. + """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). @@ -335,11 +334,11 @@ class TranslatableSetting(object): return self.values[lang] def __str__(self): - """Return the value in the currently set language. (deprecated)""" + """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 the value in the currently set language (deprecated).""" return self.values[self.get_lang()] def __repr__(self): @@ -433,8 +432,7 @@ class TranslatableSetting(object): class TemplateHookRegistry(object): - """ - A registry for template hooks. + r"""A registry for template hooks. Usage: @@ -442,7 +440,7 @@ class TemplateHookRegistry(object): >>> r.append('Hello!') >>> r.append(lambda x: 'Hello ' + x + '!', False, 'world') >>> str(r()) # str() call is not recommended in real use - 'Hello!\\nHello world!' + 'Hello!\nHello world!' >>> """ @@ -486,31 +484,47 @@ class TemplateHookRegistry(object): self._items.append((c, inp, wants_site_and_context, args, kwargs)) def __hash__(self): + """Return hash of a registry.""" return hash(config_changed({self.name: self._items})._calc_digest()) def __str__(self): + """Stringify a registry.""" return '<TemplateHookRegistry: {0}>'.format(self._items) + def __repr__(self): + """Provide the representation of a registry.""" + return '<TemplateHookRegistry: {0}>'.format(self.name) + class CustomEncoder(json.JSONEncoder): + + """Custom JSON encoder.""" + def default(self, obj): + """Default encoding handler.""" try: return super(CustomEncoder, self).default(obj) except TypeError: - s = repr(obj).split('0x', 1)[0] + if isinstance(obj, (set, frozenset)): + return self.encode(sorted(list(obj))) + else: + s = repr(obj).split('0x', 1)[0] return s class config_changed(tools.config_changed): - """ A copy of doit's but using pickle instead of serializing manually.""" + + """A copy of doit's config_changed, using pickle instead of serializing manually.""" def __init__(self, config, identifier=None): + """Initialize config_changed.""" super(config_changed, self).__init__(config) self.identifier = '_config_changed' if identifier is not None: self.identifier += ':' + identifier def _calc_digest(self): + """Calculate a config_changed digest.""" if isinstance(self.config, str): return self.config elif isinstance(self.config, dict): @@ -528,6 +542,7 @@ class config_changed(tools.config_changed): self.config))) def configure_task(self, task): + """Configure a task with a digest.""" task.value_savers.append(lambda: {self.identifier: self._calc_digest()}) def __call__(self, task, values): @@ -538,12 +553,14 @@ class config_changed(tools.config_changed): return (last_success == self._calc_digest()) def __repr__(self): + """Provide a representation of config_changed.""" return "Change with config: {0}".format(json.dumps(self.config, - cls=CustomEncoder)) + cls=CustomEncoder, + sort_keys=True)) def get_theme_path(theme, _themes_dir='themes'): - """Given a theme name, returns the path where its files are located. + """Return the path where the given theme's files are located. Looks in ./themes and in the place where themes go when installed. """ @@ -557,6 +574,7 @@ def get_theme_path(theme, _themes_dir='themes'): def get_template_engine(themes, _themes_dir='themes'): + """Get template engine used by a given theme.""" for theme_name in themes: engine_path = os.path.join(get_theme_path(theme_name, _themes_dir), 'engine') if os.path.isfile(engine_path): @@ -567,6 +585,7 @@ def get_template_engine(themes, _themes_dir='themes'): def get_parent_theme_name(theme_name, _themes_dir='themes'): + """Get name of parent theme.""" 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: @@ -587,20 +606,25 @@ def get_theme_chain(theme, _themes_dir='themes'): return themes -warned = [] +language_incomplete_warned = [] class LanguageNotFoundError(Exception): + + """An exception thrown if language is not found.""" + def __init__(self, lang, orig): + """Initialize exception.""" self.lang = lang self.orig = orig def __str__(self): + """Stringify the exception.""" return 'cannot find language {0}'.format(self.lang) def load_messages(themes, translations, default_lang): - """ Load theme's messages into context. + """Load theme's messages into context. All the messages from parent themes are loaded, and "younger" themes have priority. @@ -620,8 +644,8 @@ def load_messages(themes, translations, default_lang): _reload(translation) if sorted(translation.MESSAGES.keys()) !=\ sorted(english.MESSAGES.keys()) and \ - lang not in warned: - warned.append(lang) + lang not in language_incomplete_warned: + language_incomplete_warned.append(lang) LOGGER.warn("Incomplete translation for language " "'{0}'.".format(lang)) messages[lang].update(english.MESSAGES) @@ -673,6 +697,7 @@ def copy_tree(src, dst, link_cutoff=None): def copy_file(source, dest, cutoff=None): + """Copy a file from source to dest. If link target starts with `cutoff`, symlinks are used.""" dst_dir = os.path.dirname(dest) makedirs(dst_dir) if os.path.islink(source): @@ -693,6 +718,7 @@ def copy_file(source, dest, cutoff=None): def remove_file(source): + """Remove file or directory.""" if os.path.isdir(source): shutil.rmtree(source) elif os.path.isfile(source) or os.path.islink(source): @@ -706,13 +732,11 @@ _slugify_hyphenate_re = re.compile(r'[-\s]+') def slugify(value, force=False): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. + u"""Normalize string, convert to lowercase, remove non-alpha characters, convert spaces to hyphens. From Django's "django/template/defaultfilters.py". - >>> print(slugify('\xe1\xe9\xed.\xf3\xfa')) + >>> print(slugify('áéí.óú')) aeiou >>> print(slugify('foo/bar')) @@ -720,16 +744,15 @@ def slugify(value, force=False): >>> print(slugify('foo bar')) foo-bar - """ if not isinstance(value, unicode_str): raise ValueError("Not a unicode object: {0}".format(value)) if USE_SLUGIFY or force: # This is the standard state of slugify, which actually does some work. # It is the preferred style, especially for Western languages. - value = unidecode(value) - value = str(_slugify_strip_re.sub('', value).strip().lower()) - return _slugify_hyphenate_re.sub('-', value) + value = unicode_str(unidecode(value)) + value = _slugify_strip_re.sub('', value, re.UNICODE).strip().lower() + return _slugify_hyphenate_re.sub('-', value, re.UNICODE) else: # This is the “disarmed” state of slugify, which lets the user # have any character they please (be it regular ASCII with spaces, @@ -739,7 +762,7 @@ def slugify(value, force=False): # We still replace some characters, though. In particular, we need # to replace ? and #, which should not appear in URLs, and some # Windows-unsafe characters. This list might be even longer. - rc = '/\\?#"\'\r\n\t*:<>|"' + rc = '/\\?#"\'\r\n\t*:<>|' for c in rc: value = value.replace(c, '-') @@ -763,10 +786,14 @@ def unslugify(value, discard_numbers=True): # python < 2.6 class UnsafeZipException(Exception): + + """Exception for unsafe zip files.""" + pass def extract_all(zipfile, path='themes'): + """Extract all files from a zip file.""" pwd = os.getcwd() makedirs(path) os.chdir(path) @@ -786,6 +813,7 @@ def extract_all(zipfile, path='themes'): def to_datetime(value, tzinfo=None): + """Convert string to datetime.""" try: if not isinstance(value, datetime.datetime): # dateutil does bad things with TZs like UTC-03:00. @@ -800,8 +828,7 @@ def to_datetime(value, tzinfo=None): def get_tzname(dt): - """ - Given a datetime value, find the name of the time zone. + """Given a datetime value, find the name of the time zone. DEPRECATED: This thing returned basically the 1st random zone that matched the offset. @@ -810,6 +837,7 @@ def get_tzname(dt): def current_time(tzinfo=None): + """Get current time.""" if tzinfo is not None: dt = datetime.datetime.now(tzinfo) else: @@ -818,13 +846,12 @@ def current_time(tzinfo=None): def apply_filters(task, filters, skip_ext=None): - """ - Given a task, checks its targets. - If any of the targets has a filter that matches, + """Apply filters to a task. + + If any of the targets of the given task has a filter that matches, adds the filter commands to the commands of the task, and the filter itself to the uptodate of the task. """ - if '.php' in filters.keys(): if task_filters.php_template_injection not in filters['.php']: filters['.php'].append(task_filters.php_template_injection) @@ -862,6 +889,7 @@ def apply_filters(task, filters, skip_ext=None): def get_crumbs(path, is_file=False, index_folder=None): """Create proper links for a crumb bar. + index_folder is used if you want to use title from index file instead of folder name as breadcrumb text. @@ -889,7 +917,6 @@ def get_crumbs(path, is_file=False, index_folder=None): >>> print('|'.join(crumbs[2])) #|bar """ - crumbs = path.split(os.sep) _crumbs = [] if is_file: @@ -919,25 +946,22 @@ def get_crumbs(path, is_file=False, index_folder=None): def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='themes'): - """ - .. versionchanged:: 6.1.0 - - Checks which theme provides the path with the given asset, - and returns the "real", absolute path to the asset. + """Return the "real", absolute path to the asset. + By default, it checks which theme provides the asset. If the asset is not provided by a theme, then it will be checked for - in the FILES_FOLDERS + in the FILES_FOLDERS. - >>> print(get_asset_path('assets/css/rst.css', ['bootstrap', 'base'])) + >>> print(get_asset_path('assets/css/rst.css', ['bootstrap3', 'base'])) /.../nikola/data/themes/base/assets/css/rst.css - >>> print(get_asset_path('assets/css/theme.css', ['bootstrap', 'base'])) - /.../nikola/data/themes/bootstrap/assets/css/theme.css + >>> print(get_asset_path('assets/css/theme.css', ['bootstrap3', 'base'])) + /.../nikola/data/themes/bootstrap3/assets/css/theme.css - >>> print(get_asset_path('nikola.py', ['bootstrap', 'base'], {'nikola': ''})) + >>> print(get_asset_path('nikola.py', ['bootstrap3', 'base'], {'nikola': ''})) /.../nikola/nikola.py - >>> print(get_asset_path('nikola/nikola.py', ['bootstrap', 'base'], {'nikola':'nikola'})) + >>> print(get_asset_path('nikola/nikola.py', ['bootstrap3', 'base'], {'nikola':'nikola'})) None """ @@ -958,16 +982,20 @@ def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='theme class LocaleBorgUninitializedException(Exception): + + """Exception for unitialized LocaleBorg.""" + def __init__(self): + """Initialize exception.""" super(LocaleBorgUninitializedException, self).__init__("Attempt to use LocaleBorg before initialization") class LocaleBorg(object): - """ - Provides locale related services and autoritative current_lang, - where current_lang is the last lang for which the locale was set. - current_lang is meant to be set only by LocaleBorg.set_locale + """Provide locale related services and autoritative current_lang. + + current_lang is the last lang for which the locale was set + and is meant to be set only by LocaleBorg.set_locale. python's locale code should not be directly called from code outside of LocaleBorg, they are compatibilty issues with py version and OS support @@ -1001,7 +1029,8 @@ class LocaleBorg(object): @classmethod def initialize(cls, locales, initial_lang): - """ + """Initialize LocaleBorg. + locales : dict with lang: locale_n the same keys as in nikola's TRANSLATIONS locale_n a sanitized locale, meaning @@ -1025,19 +1054,23 @@ class LocaleBorg(object): @classmethod def reset(cls): - """used in testing to not leak state between tests""" + """Reset LocaleBorg. + + Used in testing to prevent leaking state between tests. + """ cls.locales = {} cls.encodings = {} cls.__shared_state = {'current_lang': None} cls.initialized = False def __init__(self): + """Initialize.""" if not self.initialized: raise LocaleBorgUninitializedException() self.__dict__ = self.__shared_state def set_locale(self, lang): - """Sets the locale for language lang, returns '' + """Set the locale for language lang, returns an empty string. in linux the locale encoding is set to utf8, in windows that cannot be guaranted. @@ -1053,7 +1086,7 @@ class LocaleBorg(object): return '' def get_month_name(self, month_no, lang): - """returns localized month name in an unicode string""" + """Return localized month name in an unicode string.""" if sys.version_info[0] == 3: # Python 3 with calendar.different_locale(self.locales[lang]): s = calendar.month_name[month_no] @@ -1072,15 +1105,20 @@ class LocaleBorg(object): class ExtendedRSS2(rss.RSS2): + + """Extended RSS class.""" + xsl_stylesheet_href = None def publish(self, handler): + """Publish a feed.""" if self.xsl_stylesheet_href: handler.processingInstruction("xml-stylesheet", 'type="text/xsl" href="{0}" media="all"'.format(self.xsl_stylesheet_href)) # old-style class in py2 rss.RSS2.publish(self, handler) def publish_extensions(self, handler): + """Publish extensions.""" if self.self_url: handler.startElement("atom:link", { 'href': self.self_url, @@ -1092,12 +1130,16 @@ class ExtendedRSS2(rss.RSS2): class ExtendedItem(rss.RSSItem): + """Extended RSS item.""" + def __init__(self, **kw): + """Initialize RSS item.""" self.creator = kw.pop('creator') # It's an old style class return rss.RSSItem.__init__(self, **kw) def publish_extensions(self, handler): + """Publish extensions.""" if self.creator: handler.startElement("dc:creator", {}) handler.characters(self.creator) @@ -1111,7 +1153,7 @@ explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL) def split_explicit_title(text): """Split role content into title and target, if given. - From Sphinx's "sphinx/util/nodes.py" + From Sphinx's "sphinx/util/nodes.py" """ match = explicit_title_re.match(text) if match: @@ -1120,7 +1162,7 @@ def split_explicit_title(text): def first_line(doc): - """extract first non-blank line from text, to extract docstring title""" + """Extract first non-blank line from text, to extract docstring title.""" if doc is not None: for line in doc.splitlines(): striped = line.strip() @@ -1145,7 +1187,7 @@ def demote_headers(doc, level=1): def get_root_dir(): - """Find root directory of nikola installation by looking for conf.py""" + """Find root directory of nikola site by looking for conf.py.""" root = os.getcwd() if sys.version_info[0] == 2: @@ -1167,9 +1209,7 @@ 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. + """Return a possible path where we can find the translated version of some page, 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')) @@ -1200,7 +1240,6 @@ def get_translation_candidate(config, path, lang): 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 @@ -1287,6 +1326,7 @@ def ask_yesno(query, default=None): class CommandWrapper(object): + """Converts commands into functions.""" def __init__(self, cmd, commands_object): @@ -1313,7 +1353,7 @@ class Commands(object): """ def __init__(self, main, config, doitargs): - """Takes a main instance, works as wrapper for commands.""" + """Take a main instance, work as wrapper for commands.""" self._cmdnames = [] self._main = main self._config = config @@ -1372,7 +1412,6 @@ class Commands(object): def __repr__(self): """Return useful and verbose help.""" - return """\ <Nikola Commands> @@ -1386,6 +1425,7 @@ Available commands: {0}.""".format(', '.join(self._cmdnames)) def options2docstring(name, options): + """Translate options to a docstring.""" 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)) @@ -1393,8 +1433,11 @@ def options2docstring(name, options): class NikolaPygmentsHTML(HtmlFormatter): - """A Nikola-specific modification of Pygments’ HtmlFormatter.""" + + """A Nikola-specific modification of Pygments' HtmlFormatter.""" + def __init__(self, anchor_ref, classes=None, linenos='table', linenostart=1): + """Initialize formatter.""" if classes is None: classes = ['code', 'literal-block'] self.nclasses = classes @@ -1403,11 +1446,7 @@ class NikolaPygmentsHTML(HtmlFormatter): lineanchors=slugify(anchor_ref, force=True), anchorlinenos=True) def wrap(self, source, outfile): - """ - Wrap the ``source``, which is a generator yielding - individual lines, in custom generators. - """ - + """Wrap the ``source``, which is a generator yielding individual lines, in custom generators.""" style = [] if self.prestyles: style.append(self.prestyles) @@ -1423,6 +1462,7 @@ class NikolaPygmentsHTML(HtmlFormatter): def get_displayed_page_number(i, num_pages, site): + """Get page number to be displayed for entry `i`.""" if not i: i = 0 if site.config["INDEXES_STATIC"]: @@ -1432,6 +1472,7 @@ def get_displayed_page_number(i, num_pages, site): def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force_addition=False, extension=None): + """Retrurn a path list for a given index page.""" index_file = site.config["INDEX_FILE"] if i or force_addition: path_list = list(path_list) @@ -1459,6 +1500,7 @@ def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force def os_path_split(path): + """Split a path.""" result = [] while True: previous_path = path @@ -1473,10 +1515,12 @@ def os_path_split(path): def adjust_name_for_index_path(name, i, displayed_i, lang, site, force_addition=False, extension=None): + """Return file name for a given index file.""" return os.path.join(*adjust_name_for_index_path_list(os_path_split(name), i, displayed_i, lang, site, force_addition, extension)) def adjust_name_for_index_link(name, i, displayed_i, lang, site, force_addition=False, extension=None): + """Return link for a given index file.""" link = adjust_name_for_index_path_list(name.split('/'), i, displayed_i, lang, site, force_addition, extension) if not extension == ".atom": if len(link) > 0 and link[-1] == site.config["INDEX_FILE"] and site.config["STRIP_INDEXES"]: @@ -1485,6 +1529,7 @@ def adjust_name_for_index_link(name, i, displayed_i, lang, site, force_addition= def create_redirect(src, dst): + """"Create a redirection.""" makedirs(os.path.dirname(src)) with io.open(src, "w+", encoding="utf8") as fd: fd.write('<!DOCTYPE html>\n<head>\n<meta charset="utf-8">\n' @@ -1495,6 +1540,9 @@ def create_redirect(src, dst): class TreeNode(object): + + """A tree node.""" + indent_levels = None # use for formatting comments as tree indent_change_before = 0 # use for formatting comments as tree indent_change_after = 0 # use for formatting comments as tree @@ -1530,11 +1578,13 @@ class TreeNode(object): # indent_levels property for that node.) def __init__(self, name, parent=None): + """Initialize node.""" self.name = name self.parent = parent self.children = [] def get_path(self): + """Get path.""" path = [] curr = self while curr is not None: @@ -1543,10 +1593,12 @@ class TreeNode(object): return reversed(path) def get_children(self): + """Get children of a node.""" return self.children def flatten_tree_structure(root_list): + """Flatten a tree.""" elements = [] def generate(input_list, indent_levels_so_far): @@ -1582,6 +1634,7 @@ def flatten_tree_structure(root_list): def parse_escaped_hierarchical_category_name(category_name): + """Parse a category name.""" result = [] current = None index = 0 @@ -1613,6 +1666,7 @@ def parse_escaped_hierarchical_category_name(category_name): def join_hierarchical_category_path(category_path): + """Join a category path.""" def escape(s): return s.replace('\\', '\\\\').replace('/', '\\/') @@ -1621,7 +1675,7 @@ def join_hierarchical_category_path(category_path): # Stolen from textwrap in Python 3.4.3. def indent(text, prefix, predicate=None): - """Adds 'prefix' to the beginning of selected lines in 'text'. + """Add '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, diff --git a/nikola/winutils.py b/nikola/winutils.py index 8e29f5b..3ea179b 100644 --- a/nikola/winutils.py +++ b/nikola/winutils.py @@ -24,7 +24,7 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""windows utilities to workaround problems with symlinks in a git clone""" +"""windows utilities to workaround problems with symlinks in a git clone.""" from __future__ import print_function, unicode_literals import os @@ -34,6 +34,7 @@ import io def is_file_into_dir(filename, dirname): + """Check if a file is in directory.""" try: res = not os.path.relpath(filename, dirname).startswith('.') except ValueError: @@ -42,7 +43,7 @@ def is_file_into_dir(filename, dirname): def fix_all_git_symlinked(topdir): - """inplace conversion of git symlinks to real content + """Convert 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 |
