diff options
| author | 2021-02-03 19:17:50 -0500 | |
|---|---|---|
| committer | 2021-02-03 19:17:50 -0500 | |
| commit | 475d074fd74425efbe783fad08f97f2df0c4909f (patch) | |
| tree | 2acdae53999b3c74b716efa4edb5b40311fa356a /nikola | |
| parent | cd502d52787f666fff3254d7d7e7578930c813c2 (diff) | |
| parent | 3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (diff) | |
Update upstream source from tag 'upstream/8.1.2'
Update to upstream version '8.1.2'
with Debian dir e5e966a9e6010ef70618dc9a61558fa4db35aceb
Diffstat (limited to 'nikola')
575 files changed, 21985 insertions, 10743 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py index a7f6fc6..4ead429 100644 --- a/nikola/__init__.py +++ b/nikola/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,11 +26,15 @@ """Nikola -- a modular, fast, simple, static website generator.""" -from __future__ import absolute_import import os +import sys -__version__ = '7.8.1' +__version__ = '8.1.2' DEBUG = bool(os.getenv('NIKOLA_DEBUG')) +SHOW_TRACEBACKS = bool(os.getenv('NIKOLA_SHOW_TRACEBACKS')) + +if sys.version_info[0] == 2: + raise Exception("Nikola does not support Python 2.") from .nikola import Nikola # NOQA from . import plugins # NOQA diff --git a/nikola/__main__.py b/nikola/__main__.py index f002768..8330e67 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,38 +26,37 @@ """The main function of Nikola.""" -from __future__ import print_function, unicode_literals -from collections import defaultdict +import importlib.util import os import shutil -try: - import readline # NOQA -except ImportError: - pass # This is only so raw_input/input does nicer things if it's available import sys +import textwrap import traceback +import doit.cmd_base +from collections import defaultdict -from doit.loader import generate_tasks -from doit.cmd_base import TaskLoader -from doit.reporter import ExecutedOnlyReporter -from doit.doit_cmd import DoitMain -from doit.cmd_help import Help as DoitHelp -from doit.cmd_run import Run as DoitRun +from blinker import signal +from doit.cmd_auto import Auto as DoitAuto +from doit.cmd_base import TaskLoader, _wrap from doit.cmd_clean import Clean as DoitClean from doit.cmd_completion import TabCompletion -from doit.cmd_auto import Auto as DoitAuto -from logbook import NullHandler -from blinker import signal +from doit.cmd_help import Help as DoitHelp +from doit.cmd_run import Run as DoitRun +from doit.doit_cmd import DoitMain +from doit.loader import generate_tasks +from doit.reporter import ExecutedOnlyReporter 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, STDERR_HANDLER, ColorfulStderrHandler +from .plugin_categories import Command +from .log import configure_logging, LOGGER, ColorfulFormatter, LoggingMode +from .utils import get_root_dir, req_missing, sys_decode + +try: + import readline # NOQA +except ImportError: + pass # This is only so raw_input/input does nicer things if it's available -if sys.version_info[0] == 3: - import importlib.machinery -else: - import imp config = {} @@ -68,10 +67,10 @@ _RETURN_DOITNIKOLA = False def main(args=None): """Run Nikola.""" colorful = False - if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None: + if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None and os.getenv('TERM') != 'dumb': colorful = True - ColorfulStderrHandler._colorful = colorful + ColorfulFormatter._colorful = colorful if args is None: args = sys.argv[1:] @@ -80,29 +79,24 @@ def main(args=None): args = [sys_decode(arg) for arg in args] conf_filename = 'conf.py' - conf_filename_bytes = b'conf.py' conf_filename_changed = False for index, arg in enumerate(args): if arg[:7] == '--conf=': del args[index] del oargs[index] conf_filename = arg[7:] - conf_filename_bytes = sys_encode(arg[7:]) conf_filename_changed = True 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().push_application() + LOGGER.info('Running in strict mode') + configure_logging(LoggingMode.STRICT) + elif len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args: + configure_logging(LoggingMode.QUIET) quiet = True - if not quiet and not strict: - NullHandler().push_application() - STDERR_HANDLER[0].push_application() + else: + configure_logging() global config @@ -119,20 +113,22 @@ def main(args=None): os.chdir(root) # Help and imports don't require config, but can use one if it exists needs_config_file = (argname != 'help') and not argname.startswith('import_') + LOGGER.debug("Website root: %r", root) else: needs_config_file = False - sys.path.append('') + sys.path.insert(0, os.path.dirname(conf_filename)) try: - if sys.version_info[0] == 3: - loader = importlib.machinery.SourceFileLoader("conf", conf_filename) - conf = loader.load_module() - else: - conf = imp.load_source("conf", conf_filename_bytes) + spec = importlib.util.spec_from_file_location("conf", conf_filename) + conf = importlib.util.module_from_spec(spec) + # Preserve caching behavior of `import conf` if the filename matches + if os.path.splitext(os.path.basename(conf_filename))[0] == "conf": + sys.modules["conf"] = conf + spec.loader.exec_module(conf) config = conf.__dict__ except Exception: if os.path.exists(conf_filename): - msg = traceback.format_exc(0) + msg = traceback.format_exc() LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg)) return 1 elif needs_config_file and conf_filename_changed: @@ -155,7 +151,7 @@ def main(args=None): req_missing(['freezegun'], 'perform invariant builds') if config: - if os.path.exists('plugins') and not os.path.exists('plugins/__init__.py'): + if os.path.isdir('plugins') and not os.path.exists('plugins/__init__.py'): with open('plugins/__init__.py', 'w') as fh: fh.write('# Plugin modules go here.') @@ -233,19 +229,21 @@ class Build(DoitRun): } ) self.cmd_options = tuple(opts) - super(Build, self).__init__(*args, **kw) + super().__init__(*args, **kw) class Clean(DoitClean): """Clean site, including the cache directory.""" - def clean_tasks(self, tasks, dryrun): + # The unseemly *a is because this API changed between doit 0.30.1 and 0.31 + def clean_tasks(self, tasks, dryrun, *a): """Clean tasks.""" if not dryrun and config: cache_folder = config.get('CACHE_FOLDER', 'cache') if os.path.exists(cache_folder): shutil.rmtree(cache_folder) - return super(Clean, self).clean_tasks(tasks, dryrun) + return super(Clean, self).clean_tasks(tasks, dryrun, *a) + # Nikola has its own "auto" commands that uses livereload. # Expose original doit "auto" command as "doit_auto". @@ -274,13 +272,20 @@ class NikolaTaskLoader(TaskLoader): } DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render'] DOIT_CONFIG.update(self.nikola._doit_config) - tasks = generate_tasks( - 'render_site', - self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.')) - latetasks = generate_tasks( - 'post_render', - self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.')) - signal('initialized').send(self.nikola) + try: + tasks = generate_tasks( + 'render_site', + self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.')) + latetasks = generate_tasks( + 'post_render', + self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.')) + signal('initialized').send(self.nikola) + except Exception: + LOGGER.error('Error loading tasks. An unhandled exception occurred.') + if self.nikola.debug or self.nikola.show_tracebacks: + raise + _print_exception() + sys.exit(3) return tasks + latetasks, DOIT_CONFIG @@ -293,7 +298,7 @@ class DoitNikola(DoitMain): def __init__(self, nikola, quiet=False): """Initialzie DoitNikola.""" - super(DoitNikola, self).__init__() + super().__init__() self.nikola = nikola nikola.doit = self self.task_loader = self.TASK_LOADER(nikola, quiet) @@ -362,7 +367,14 @@ class DoitNikola(DoitMain): LOGGER.error("This command needs to run inside an " "existing Nikola site.") return 3 - return super(DoitNikola, self).run(cmd_args) + try: + return super().run(cmd_args) + except Exception: + LOGGER.error('An unhandled exception occurred.') + if self.nikola.debug or self.nikola.show_tracebacks: + raise + _print_exception() + return 1 @staticmethod def print_version(): @@ -370,6 +382,53 @@ class DoitNikola(DoitMain): print("Nikola v" + __version__) +# Override Command.help() to make it more readable and to remove +# some doit-specific stuff. Based on doit's implementation. +# (see Issue #3342) +def _command_help(self: Command): + """Return help text for a command.""" + text = [] + + usage = "{} {} {}".format(self.bin_name, self.name, self.doc_usage) + text.extend(textwrap.wrap(usage, subsequent_indent=' ')) + text.extend(_wrap(self.doc_purpose, 4)) + + text.append("\nOptions:") + options = defaultdict(list) + for opt in self.cmdparser.options: + options[opt.section].append(opt) + for section, opts in sorted(options.items()): + if section: + section_name = '\n{}'.format(section) + text.extend(_wrap(section_name, 2)) + for opt in opts: + # ignore option that cant be modified on cmd line + if not (opt.short or opt.long): + continue + text.extend(_wrap(opt.help_param(), 4)) + opt_help = opt.help + if '%(default)s' in opt_help: + opt_help = opt.help % {'default': opt.default} + elif opt.default != '' and opt.default is not False and opt.default is not None: + opt_help += ' [default: {}]'.format(opt.default) + opt_choices = opt.help_choices() + desc = '{} {}'.format(opt_help, opt_choices) + text.extend(_wrap(desc, 8)) + + # print bool inverse option + if opt.inverse: + text.extend(_wrap('--{}'.format(opt.inverse), 4)) + text.extend(_wrap('opposite of --{}'.format(opt.long), 8)) + + if self.doc_description is not None: + text.append("\n\nDescription:") + text.extend(_wrap(self.doc_description, 4)) + return "\n".join(text) + + +doit.cmd_base.Command.help = _command_help + + def levenshtein(s1, s2): u"""Calculate the Levenshtein distance of two strings. @@ -398,5 +457,12 @@ def levenshtein(s1, s2): return previous_row[-1] +def _print_exception(): + """Print an exception in a friendlier, shorter style.""" + etype, evalue, _ = sys.exc_info() + LOGGER.error(''.join(traceback.format_exception(etype, evalue, None, limit=0, chain=False)).strip()) + LOGGER.warning("To see more details, run Nikola in debug mode (set environment variable NIKOLA_DEBUG=1) or use NIKOLA_SHOW_TRACEBACKS=1") + + if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/nikola/conf.py.in b/nikola/conf.py.in index 5010278..4546460 100644 --- a/nikola/conf.py.in +++ b/nikola/conf.py.in @@ -1,7 +1,6 @@ ## -*- coding: utf-8 -*- # -*- coding: utf-8 -*- -from __future__ import unicode_literals import time # !! This is the configuration of Nikola. !! # @@ -57,7 +56,7 @@ TRANSLATIONS = ${TRANSLATIONS} # this pattern is also used for metadata: # something.meta -> something.pl.meta -TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN} +TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}' # Links for the sidebar / navigation bar. (translatable) # This is a dict. The keys are languages, and values are tuples. @@ -76,9 +75,9 @@ TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN} # # WARNING: Support for submenus is theme-dependent. # Only one level of submenus is supported. -# WARNING: Some themes, including the default Bootstrap 3 theme, +# WARNING: Some themes, including the default Bootstrap 4 theme, # may present issues if the menu is too large. -# (in bootstrap3, the navbar can grow too large and cover contents.) +# (in Bootstrap, the navbar can grow too large and cover contents.) # WARNING: If you link to directories, make sure to follow # ``STRIP_INDEXES``. If it’s set to ``True``, end your links # with a ``/``, otherwise end them with ``/index.html`` — or @@ -86,14 +85,61 @@ TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN} NAVIGATION_LINKS = ${NAVIGATION_LINKS} +# Alternative navigation links. Works the same way NAVIGATION_LINKS does, +# although themes may not always support them. (translatable) +# (Bootstrap 4: right-side of navbar, Bootblog 4: right side of title) +NAVIGATION_ALT_LINKS = { + DEFAULT_LANG: () +} + # Name of the theme to use. THEME = ${THEME} -# Primary color of your theme. This will be used to customize your theme and -# auto-generate related colors in POSTS_SECTION_COLORS. Must be a HEX value. +# A theme color. In default themes, it might be displayed by some browsers as +# the browser UI color (eg. Chrome on Android). Other themes might also use it +# as an accent color (the default ones don’t). Must be a HEX value. THEME_COLOR = '#5670d4' +# Theme configuration. Fully theme-dependent. (translatable) +# Samples for bootblog4 (enabled) and bootstrap4 (commented) follow. +# bootblog4 supports: featured_large featured_small featured_on_mobile +# featured_large_image_on_mobile featured_strip_html sidebar +# bootstrap4 supports: navbar_light (defaults to False) +# navbar_custom_bg (defaults to '') + +# Config for bootblog4: +THEME_CONFIG = { + DEFAULT_LANG: { + # Show the latest featured post in a large box, with the previewimage as its background. + 'featured_large': False, + # Show the first (remaining) two featured posts in small boxes. + 'featured_small': False, + # Show featured posts on mobile. + 'featured_on_mobile': True, + # Show image in `featured_large` on mobile. + # `featured_small` displays them only on desktop. + 'featured_large_image_on_mobile': True, + # Strip HTML from featured post text. + 'featured_strip_html': False, + # Contents of the sidebar, If empty, the sidebar is not displayed. + 'sidebar': '' + } +} +# Config for bootstrap4: +# THEME_CONFIG = { +# DEFAULT_LANG: { +# # Use a light navbar with dark text. Defaults to False. +# 'navbar_light': False, +# # Use a custom navbar color. If unset, 'navbar_light' sets text + +# # background color. If set, navbar_light controls only background +# # color. Supported values: bg-dark, bg-light, bg-primary, bg-secondary, +# # bg-success, bg-danger, bg-warning, bg-info, bg-white, bg-transparent. +# 'navbar_custom_bg': '', +# } +# } + # POSTS and PAGES contains (wildcard, destination, template) tuples. +# (translatable) # # The wildcard is used to generate a list of source files # (whatever/thing.rst, for example). @@ -119,6 +165,12 @@ THEME_COLOR = '#5670d4' # to feeds, indexes, tag lists and archives and are considered part # of a blog, while PAGES are just independent HTML pages. # +# Finally, note that destination can be translated, i.e. you can +# specify a different translation folder per language. Example: +# PAGES = ( +# ("pages/*.rst", {"en": "pages", "de": "seiten"}, "page.tmpl"), +# ("pages/*.md", {"en": "pages", "de": "seiten"}, "page.tmpl"), +# ) POSTS = ${POSTS} PAGES = ${PAGES} @@ -143,36 +195,29 @@ TIMEZONE = ${TIMEZONE} # FORCE_ISO8601 = False # Date format used to display post dates. (translatable) -# (str used by datetime.datetime.strftime) -# DATE_FORMAT = '%Y-%m-%d %H:%M' +# Used by babel.dates, CLDR style: http://cldr.unicode.org/translation/date-time +# You can also use 'full', 'long', 'medium', or 'short' +# DATE_FORMAT = 'yyyy-MM-dd HH:mm' # Date format used to display post dates, if local dates are used. (translatable) -# (str used by moment.js) -# JS_DATE_FORMAT = 'YYYY-MM-DD HH:mm' +# Used by Luxon: https://moment.github.io/luxon/docs/manual/formatting +# Example for presets: {'preset': True, 'format': 'DATE_FULL'} +# LUXON_DATE_FORMAT = { +# DEFAULT_LANG: {'preset': False, 'format': 'yyyy-MM-dd HH:mm'}, +# } # Date fanciness. # -# 0 = using DATE_FORMAT and TIMEZONE -# 1 = using JS_DATE_FORMAT and local user time (via moment.js) -# 2 = using a string like “2 days ago” +# 0 = using DATE_FORMAT and TIMEZONE (without JS) +# 1 = using LUXON_DATE_FORMAT and local user time (JS, using Luxon) +# 2 = using a string like “2 days ago” (JS, using Luxon) # -# Your theme must support it, bootstrap and bootstrap3 already do. +# Your theme must support it, Bootstrap already does. # DATE_FANCINESS = 0 -# While Nikola can select a sensible locale for each language, -# sometimes explicit control can come handy. -# In this file we express locales in the string form that -# python's locales will accept in your OS, by example -# "en_US.utf8" in Unix-like OS, "English_United States" in Windows. -# LOCALES = dict mapping language --> explicit locale for the languages -# in TRANSLATIONS. You can omit one or more keys. -# LOCALE_FALLBACK = locale to use when an explicit locale is unavailable -# LOCALE_DEFAULT = locale to use for languages not mentioned in LOCALES; if -# not set the default Nikola mapping is used. - +# Customize the locale/region used for a language. +# For example, to use British instead of US English: LOCALES = {'en': 'en_GB'} # LOCALES = {} -# LOCALE_FALLBACK = None -# LOCALE_DEFAULT = None # One or more folders containing files to be copied as-is into the output. # The format is a dictionary of {source: relative destination}. @@ -190,20 +235,42 @@ TIMEZONE = ${TIMEZONE} # Feel free to add or delete extensions to any list, but don't add any new # compilers unless you write the interface for it yourself. # +# The default compiler for `new_post` is the first entry in the POSTS tuple. +# # 'rest' is reStructuredText -# 'markdown' is MarkDown +# 'markdown' is Markdown # 'html' assumes the file is HTML and just copies it COMPILERS = ${COMPILERS} +# Enable reST directives that insert the contents of external files such +# as "include" and "raw." This maps directly to the docutils file_insertion_enabled +# config. See: http://docutils.sourceforge.net/docs/user/config.html#file-insertion-enabled +# REST_FILE_INSERTION_ENABLED = True + # Create by default posts in one file format? # Set to False for two-file posts, with separate metadata. # ONE_FILE_POSTS = True +# Preferred metadata format for new posts +# "Nikola": reST comments, wrapped in a HTML comment if needed (default) +# "YAML": YAML wrapped in "---" +# "TOML": TOML wrapped in "+++" +# "Pelican": Native markdown metadata or reST docinfo fields. Nikola style for other formats. +# METADATA_FORMAT = "Nikola" + +# Use date-based path when creating posts? +# Can be enabled on a per-post basis with `nikola new_post -d`. +# The setting is ignored when creating pages. +# NEW_POST_DATE_PATH = False + +# What format to use when creating posts with date paths? +# Default is '%Y/%m/%d', other possibilities include '%Y' or '%Y/%m'. +# NEW_POST_DATE_PATH_FORMAT = '%Y/%m/%d' + # If this is set to True, the DEFAULT_LANG version will be displayed for # untranslated posts. # If this is set to False, then posts that are not translated to a language # LANG will not be visible at all in the pages in that language. -# Formerly known as HIDE_UNTRANSLATED_POSTS (inverse) # SHOW_UNTRANSLATED_POSTS = True # Nikola supports logo display. If you have one, you can put the URL here. @@ -211,77 +278,33 @@ COMPILERS = ${COMPILERS} # The URL may be relative to the site root. # LOGO_URL = '' +# When linking posts to social media, Nikola provides Open Graph metadata +# which is used to show a nice preview. This includes an image preview +# taken from the post's previewimage metadata field. +# This option lets you use an image to be used if the post doesn't have it. +# The default is None, valid values are URLs or output paths like +# "/images/foo.jpg" +# DEFAULT_PREVIEW_IMAGE = None + # If you want to hide the title of your website (for example, if your logo # already contains the text), set this to False. # SHOW_BLOG_TITLE = True -# Writes tag cloud data in form of tag_cloud_data.json. -# Warning: this option will change its default value to False in v8! -WRITE_TAG_CLOUD = True - -# Generate pages for each section. The site must have at least two sections -# for this option to take effect. It wouldn't build for just one section. -POSTS_SECTIONS = True - -# Setting this to False generates a list page instead of an index. Indexes -# are the default and will apply GENERATE_ATOM if set. -# POSTS_SECTIONS_ARE_INDEXES = True - -# Each post and section page will have an associated color that can be used -# to style them with a recognizable color detail across your site. A color -# is assigned to each section based on shifting the hue of your THEME_COLOR -# at least 7.5 % while leaving the lightness and saturation untouched in the -# HUSL colorspace. You can overwrite colors by assigning them colors in HEX. -# POSTS_SECTION_COLORS = { -# DEFAULT_LANG: { -# 'posts': '#49b11bf', -# 'reviews': '#ffe200', -# }, -# } - -# Associate a description with a section. For use in meta description on -# section index pages or elsewhere in themes. -# POSTS_SECTION_DESCRIPTIONS = { -# DEFAULT_LANG: { -# 'how-to': 'Learn how-to things properly with these amazing tutorials.', -# }, -# } - -# Sections are determined by their output directory as set in POSTS by default, -# but can alternatively be determined from file metadata instead. -# POSTS_SECTION_FROM_META = False - -# Names are determined from the output directory name automatically or the -# metadata label. Unless overwritten below, names will use title cased and -# hyphens replaced by spaces. -# POSTS_SECTION_NAME = { -# DEFAULT_LANG: { -# 'posts': 'Blog Posts', -# 'uncategorized': 'Odds and Ends', -# }, -# } - -# Titles for per-section index pages. Can be either one string where "{name}" -# is substituted or the POSTS_SECTION_NAME, or a dict of sections. Note -# that the INDEX_PAGES option is also applied to section page titles. -# POSTS_SECTION_TITLE = { -# DEFAULT_LANG: { -# 'how-to': 'How-to and Tutorials', -# }, -# } - # Paths for different autogenerated bits. These are combined with the # translation paths. # Final locations are: # output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) # output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) -# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) +# output / TRANSLATION[lang] / TAG_PATH / tag RSS_EXTENSION (RSS feed for a tag) # (translatable) # TAG_PATH = "categories" -# See TAG_PATH's "list of tags" for the default setting value. Can be overwritten -# here any path relative to the output directory. +# By default, the list of tags is stored in +# output / TRANSLATION[lang] / TAG_PATH / index.html +# (see explanation for TAG_PATH). This location can be changed to +# output / TRANSLATION[lang] / TAGS_INDEX_PATH +# with an arbitrary relative path TAGS_INDEX_PATH. # (translatable) # TAGS_INDEX_PATH = "tags.html" @@ -292,15 +315,15 @@ POSTS_SECTIONS = True # Set descriptions for tag pages to make them more interesting. The # default is no description. The value is used in the meta description # and displayed underneath the tag list or index page’s title. -# TAG_PAGES_DESCRIPTIONS = { +# TAG_DESCRIPTIONS = { # DEFAULT_LANG: { -# "blogging": "Meta-blog posts about blogging about blogging.", +# "blogging": "Meta-blog posts about blogging.", # "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." # }, # } # Set special titles for tag pages. The default is "Posts about TAG". -# TAG_PAGES_TITLES = { +# TAG_TITLES = { # DEFAULT_LANG: { # "blogging": "Meta-posts about blogging", # "open source": "Posts about open source software" @@ -308,7 +331,7 @@ POSTS_SECTIONS = True # } # If you do not want to display a tag publicly, you can mark it as hidden. -# The tag will not be displayed on the tag list page, the tag cloud and posts. +# The tag will not be displayed on the tag list page and posts. # Tag pages will still be generated. HIDDEN_TAGS = ['mathjax'] @@ -318,14 +341,36 @@ HIDDEN_TAGS = ['mathjax'] # However, more obscure tags can be hidden from the tag index page. # TAGLIST_MINIMUM_POSTS = 1 +# A list of dictionaries specifying tags which translate to each other. +# Format: a list of dicts {language: translation, language2: translation2, …} +# For example: +# [ +# {'en': 'private', 'de': 'Privat'}, +# {'en': 'work', 'fr': 'travail', 'de': 'Arbeit'}, +# ] +# TAG_TRANSLATIONS = [] + +# If set to True, a tag in a language will be treated as a translation +# of the literally same tag in all other languages. Enable this if you +# do not translate tags, for example. +# TAG_TRANSLATIONS_ADD_DEFAULTS = True + # Final locations are: # output / TRANSLATION[lang] / CATEGORY_PATH / index.html (list of categories) # output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.html (list of posts for a category) -# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.xml (RSS feed for a category) +# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category RSS_EXTENSION (RSS feed for a category) # (translatable) # CATEGORY_PATH = "categories" # CATEGORY_PREFIX = "cat_" +# By default, the list of categories is stored in +# output / TRANSLATION[lang] / CATEGORY_PATH / index.html +# (see explanation for CATEGORY_PATH). This location can be changed to +# output / TRANSLATION[lang] / CATEGORIES_INDEX_PATH +# with an arbitrary relative path CATEGORIES_INDEX_PATH. +# (translatable) +# CATEGORIES_INDEX_PATH = "categories.html" + # If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in # hierarchies. For a post, the whole path in the hierarchy must be specified, # using a forward slash ('/') to separate paths. Use a backslash ('\') to escape @@ -343,15 +388,15 @@ CATEGORY_OUTPUT_FLAT_HIERARCHY = ${CATEGORY_OUTPUT_FLAT_HIERARCHY} # Set descriptions for category pages to make them more interesting. The # default is no description. The value is used in the meta description # and displayed underneath the category list or index page’s title. -# CATEGORY_PAGES_DESCRIPTIONS = { +# CATEGORY_DESCRIPTIONS = { # DEFAULT_LANG: { -# "blogging": "Meta-blog posts about blogging about blogging.", +# "blogging": "Meta-blog posts about blogging.", # "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." # }, # } # Set special titles for category pages. The default is "Posts about CATEGORY". -# CATEGORY_PAGES_TITLES = { +# CATEGORY_TITLES = { # DEFAULT_LANG: { # "blogging": "Meta-posts about blogging", # "open source": "Posts about open source software" @@ -363,14 +408,56 @@ CATEGORY_OUTPUT_FLAT_HIERARCHY = ${CATEGORY_OUTPUT_FLAT_HIERARCHY} # Category pages will still be generated. HIDDEN_CATEGORIES = [] +# A list of dictionaries specifying categories which translate to each other. +# Format: a list of dicts {language: translation, language2: translation2, …} +# See TAG_TRANSLATIONS example above. +# CATEGORY_TRANSLATIONS = [] + +# If set to True, a category in a language will be treated as a translation +# of the literally same category in all other languages. Enable this if you +# do not translate categories, for example. +# CATEGORY_TRANSLATIONS_ADD_DEFAULTS = True + +# If no category is specified in a post, the destination path of the post +# can be used in its place. This replaces the sections feature. Using +# category hierarchies is recommended. +# CATEGORY_DESTPATH_AS_DEFAULT = False + +# If True, the prefix will be trimmed from the category name, eg. if the +# POSTS destination is "foo/bar", and the path is "foo/bar/baz/quux", +# the category will be "baz/quux" (or "baz" if only the first directory is considered). +# Note that prefixes coming from translations are always ignored. +# CATEGORY_DESTPATH_TRIM_PREFIX = False + +# If True, only the first directory of a path will be used. +# CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY = True + +# Map paths to prettier category names. (translatable) +# CATEGORY_DESTPATH_NAMES = { +# DEFAULT_LANG: { +# 'webdev': 'Web Development', +# 'webdev/django': 'Web Development/Django', +# 'random': 'Odds and Ends', +# }, +# } + +# By default, category indexes will appear in CATEGORY_PATH and use +# CATEGORY_PREFIX. If this is enabled, those settings will be ignored (except +# for the index) and instead, they will follow destination paths (eg. category +# 'foo' might appear in 'posts/foo'). If the category does not come from a +# destpath, first entry in POSTS followed by the category name will be used. +# For this setting, category hierarchies are required and cannot be flattened. +# CATEGORY_PAGES_FOLLOW_DESTPATH = False + # If ENABLE_AUTHOR_PAGES is set to True and there is more than one # author, author pages are generated. # ENABLE_AUTHOR_PAGES = True -# Final locations are: -# output / TRANSLATION[lang] / AUTHOR_PATH / index.html (list of tags) -# output / TRANSLATION[lang] / AUTHOR_PATH / author.html (list of posts for a tag) -# output / TRANSLATION[lang] / AUTHOR_PATH / author.xml (RSS feed for a tag) +# Path to author pages. Final locations are: +# output / TRANSLATION[lang] / AUTHOR_PATH / index.html (list of authors) +# output / TRANSLATION[lang] / AUTHOR_PATH / author.html (list of posts by an author) +# output / TRANSLATION[lang] / AUTHOR_PATH / author RSS_EXTENSION (RSS feed for an author) +# (translatable) # AUTHOR_PATH = "authors" # If AUTHOR_PAGES_ARE_INDEXES is set to True, each author's page will contain @@ -393,8 +480,12 @@ HIDDEN_CATEGORIES = [] # Tag pages will still be generated. HIDDEN_AUTHORS = ['Guest'] +# Allow multiple, comma-separated authors for a post? (Requires theme support, present in built-in themes) +# MULTIPLE_AUTHORS_PER_POST = False + # Final location for the main blog page and sibling paginated pages is # output / TRANSLATION[lang] / INDEX_PATH / index-*.html +# (translatable) # INDEX_PATH = "" # Optional HTML that displayed on “main” blog index.html files. @@ -412,11 +503,14 @@ FRONT_INDEX_HEADER = { # CREATE_FULL_ARCHIVES = False # If monthly archives or full archives are created, adds also one archive per day # CREATE_DAILY_ARCHIVE = False +# Create previous, up, next navigation links for archives +# CREATE_ARCHIVE_NAVIGATION = False # Final locations for the archives are: # output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / index.html # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / DAY / index.html +# (translatable) # ARCHIVE_PATH = "" # ARCHIVE_FILENAME = "archive.html" @@ -431,20 +525,30 @@ FRONT_INDEX_HEADER = { # absolute: a complete URL (that includes the SITE_URL) # URL_TYPE = 'rel_path' -# If USE_BASE_TAG is True, then all HTML files will include -# something like <base href=http://foo.var.com/baz/bat> to help -# the browser resolve relative links. -# Most people don’t need this tag; major websites don’t use it. Use -# only if you know what you’re doing. If this is True, your website -# will not be fully usable by manually opening .html files in your web -# browser (`nikola serve` or `nikola auto` is mandatory). Also, if you -# have mirrors of your site, they will point to SITE_URL everywhere. -USE_BASE_TAG = False +# Extension for RSS feed files +# RSS_EXTENSION = ".xml" + +# RSS filename base (without extension); used for indexes and galleries. +# (translatable) +# RSS_FILENAME_BASE = "rss" # Final location for the blog main RSS feed is: -# output / TRANSLATION[lang] / RSS_PATH / rss.xml +# output / TRANSLATION[lang] / RSS_PATH / RSS_FILENAME_BASE RSS_EXTENSION +# (translatable) # RSS_PATH = "" +# Final location for the blog main Atom feed is: +# output / TRANSLATION[lang] / ATOM_PATH / ATOM_FILENAME_BASE ATOM_EXTENSION +# (translatable) +# ATOM_PATH = "" + +# Atom filename base (without extension); used for indexes. +# (translatable) +ATOM_FILENAME_BASE = "feed" + +# Extension for Atom feed files +# ATOM_EXTENSION = ".atom" + # Slug the Tag URL. Easier for users to type, special characters are # often removed or replaced as well. # SLUG_TAG_PATH = True @@ -531,6 +635,35 @@ GITHUB_COMMIT_SOURCE = True # ".jpg": ["jpegoptim --strip-all -m75 -v %s"], # } +# Executable for the "yui_compressor" filter (defaults to 'yui-compressor'). +# YUI_COMPRESSOR_EXECUTABLE = 'yui-compressor' + +# Executable for the "closure_compiler" filter (defaults to 'closure-compiler'). +# CLOSURE_COMPILER_EXECUTABLE = 'closure-compiler' + +# Executable for the "optipng" filter (defaults to 'optipng'). +# OPTIPNG_EXECUTABLE = 'optipng' + +# Executable for the "jpegoptim" filter (defaults to 'jpegoptim'). +# JPEGOPTIM_EXECUTABLE = 'jpegoptim' + +# Executable for the "html_tidy_withconfig", "html_tidy_nowrap", +# "html_tidy_wrap", "html_tidy_wrap_attr" and "html_tidy_mini" filters +# (defaults to 'tidy5'). +# HTML_TIDY_EXECUTABLE = 'tidy5' + +# List of XPath expressions which should be used for finding headers +# ({hx} is replaced by headers h1 through h6). +# You must change this if you use a custom theme that does not use +# "e-content entry-content" as a class for post and page contents. +# HEADER_PERMALINKS_XPATH_LIST = ['*//div[@class="e-content entry-content"]//{hx}'] +# Include *every* header (not recommended): +# HEADER_PERMALINKS_XPATH_LIST = ['*//{hx}'] + +# File blacklist for header permalinks. Contains output path +# (eg. 'output/index.html') +# HEADER_PERMALINKS_FILE_BLACKLIST = [] + # Expert setting! Create a gzipped copy of each generated file. Cheap server- # side optimization for very high traffic sites or low memory servers. # GZIP_FILES = False @@ -544,20 +677,6 @@ GITHUB_COMMIT_SOURCE = True # return partial content of another representation for these resources. Do not # use this feature if you do not understand what this means. -# Compiler to process LESS files. -# LESS_COMPILER = 'lessc' - -# A list of options to pass to the LESS compiler. -# Final command is: LESS_COMPILER LESS_OPTIONS file.less -# LESS_OPTIONS = [] - -# Compiler to process Sass files. -# SASS_COMPILER = 'sass' - -# A list of options to pass to the Sass compiler. -# Final command is: SASS_COMPILER SASS_OPTIONS file.s(a|c)ss -# SASS_OPTIONS = [] - # ############################################################################# # Image Gallery Options # ############################################################################# @@ -573,7 +692,16 @@ GITHUB_COMMIT_SOURCE = True # MAX_IMAGE_SIZE = 1280 # USE_FILENAME_AS_TITLE = True # EXTRA_IMAGE_EXTENSIONS = [] -# + +# Use a thumbnail (defined by ".. previewimage:" in the gallery's index) in +# list of galleries for each gallery +GALLERIES_USE_THUMBNAIL = False + +# Image to use as thumbnail for those galleries that don't have one +# None: show a grey square +# '/url/to/file': show the image in that url +GALLERIES_DEFAULT_THUMBNAIL = None + # If set to False, it will sort by filename instead. Defaults to True # GALLERY_SORT_BY_DATE = True @@ -613,6 +741,10 @@ GITHUB_COMMIT_SOURCE = True # Embedded thumbnail information: # EXIF_WHITELIST['1st'] = ["*"] +# If set to True, any ICC profile will be copied when an image is thumbnailed or +# resized. +# PRESERVE_ICC_PROFILES = False + # Folders containing images to be used in normal posts or pages. # IMAGE_FOLDERS is a dictionary of the form {"source": "destination"}, # where "source" is the folder containing the images to be published, and @@ -622,17 +754,19 @@ GITHUB_COMMIT_SOURCE = True # To reference the images in your posts, include a leading slash in the path. # For example, if IMAGE_FOLDERS = {'images': 'images'}, write # -# ..image:: /images/tesla.jpg +# .. image:: /images/tesla.jpg # # See the Nikola Handbook for details (in the “Embedding Images” and # “Thumbnails” sections) # Images will be scaled down according to IMAGE_THUMBNAIL_SIZE and MAX_IMAGE_SIZE # options, but will have to be referenced manually to be visible on the site -# (the thumbnail has ``.thumbnail`` added before the file extension). +# (the thumbnail has ``.thumbnail`` added before the file extension by default, +# but a different naming template can be configured with IMAGE_THUMBNAIL_FORMAT). IMAGE_FOLDERS = {'images': 'images'} # IMAGE_THUMBNAIL_SIZE = 400 +# IMAGE_THUMBNAIL_FORMAT = '{name}.thumbnail{ext}' # ############################################################################# # HTML fragments and diverse things that are used by the templates @@ -680,49 +814,28 @@ IMAGE_FOLDERS = {'images': 'images'} # for the full URL with the page number of the main page to the normal (shorter) main # page URL. # INDEXES_PRETTY_PAGE_URL = False +# +# If the following is true, a page range navigation will be inserted to indices. +# Please note that this will undo the effect of INDEXES_STATIC, as all index pages +# must be recreated whenever the number of pages changes. +# SHOW_INDEX_PAGE_NAVIGATION = False + +# If the following is True, a meta name="generator" tag is added to pages. The +# generator tag is used to specify the software used to generate the page +# (it promotes Nikola). +# META_GENERATOR_TAG = True # Color scheme to be used for code blocks. If your theme provides -# "assets/css/code.css" this is ignored. +# "assets/css/code.css" this is ignored. Set to None to disable. # Can be any of: -# algol -# algol_nu -# arduino -# autumn -# borland -# bw -# colorful -# default -# emacs -# friendly -# fruity -# igor -# lovelace -# manni -# monokai -# murphy -# native -# paraiso_dark -# paraiso_light -# pastie -# perldoc -# rrt -# tango -# trac -# vim -# vs -# xcode +# algol, algol_nu, autumn, borland, bw, colorful, default, emacs, friendly, +# fruity, igor, lovelace, manni, monokai, murphy, native, paraiso-dark, +# paraiso-light, pastie, perldoc, rrt, tango, trac, vim, vs, xcode # This list MAY be incomplete since pygments adds styles every now and then. +# Check with list(pygments.styles.get_all_styles()) in an interpreter. +# # CODE_COLOR_SCHEME = 'default' -# If you use 'site-reveal' theme you can select several subthemes -# THEME_REVEAL_CONFIG_SUBTHEME = 'sky' -# You can also use: beige/serif/simple/night/default - -# Again, if you use 'site-reveal' theme you can select several transitions -# between the slides -# THEME_REVEAL_CONFIG_TRANSITION = 'cube' -# You can also use: page/concave/linear/none/default - # FAVICONS contains (name, file, size) tuples. # Used to create favicon link like this: # <link rel="name" href="file" sizes="size"/> @@ -743,6 +856,7 @@ IMAGE_FOLDERS = {'images': 'images'} # {min_remaining_read} The string “{remaining_reading_time} min remaining to read” in the current language. # {paragraph_count} The amount of paragraphs in the post. # {remaining_paragraph_count} The amount of paragraphs in the post, sans the teaser. +# {post_title} The title of the post. # {{ A literal { (U+007B LEFT CURLY BRACKET) # }} A literal } (U+007D RIGHT CURLY BRACKET) @@ -786,6 +900,8 @@ CONTENT_FOOTER = 'Contents © {date} \ # tuples of tuples of positional arguments and dicts of keyword arguments # to format(). For example, {'en': (('Hello'), {'target': 'World'})} # results in CONTENT_FOOTER['en'].format('Hello', target='World'). +# If you need to use the literal braces '{' and '}' in your footer text, use +# '{{' and '}}' to escape them (str.format is used) # WARNING: If you do not use multiple languages with CONTENT_FOOTER, this # still needs to be a dict of this format. (it can be empty if you # do not need formatting) @@ -802,6 +918,12 @@ CONTENT_FOOTER_FORMATS = { ) } +# A simple copyright tag for inclusion in RSS feeds that works just +# like CONTENT_FOOTER and CONTENT_FOOTER_FORMATS +RSS_COPYRIGHT = 'Contents © {date} <a href="mailto:{email}">{author}</a> {license}' +RSS_COPYRIGHT_PLAIN = 'Contents © {date} {author} {license}' +RSS_COPYRIGHT_FORMATS = CONTENT_FOOTER_FORMATS + # To use comments, you can choose between different third party comment # systems. The following comment systems are supported by Nikola: ${_SUPPORTED_COMMENT_SYSTEMS} @@ -813,13 +935,6 @@ COMMENT_SYSTEM = ${COMMENT_SYSTEM} # is in the manual. COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} -# Enable annotations using annotateit.org? -# If set to False, you can still enable them for individual posts and pages -# setting the "annotations" metadata. -# If set to True, you can disable them for individual posts and pages using -# the "noannotations" metadata. -# ANNOTATIONS = False - # Create index.html for page folders? # WARNING: if a page would conflict with the index file (usually # caused by setting slug to `index`), the PAGE_INDEX @@ -839,17 +954,8 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} # http://mysite/foo/bar/index.html => http://mysite/foo/bar/ # (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) STRIP_INDEXES = ${STRIP_INDEXES} -# Should the sitemap list directories which only include other directories -# and no files. -# Default to True -# If this is False -# e.g. /2012 includes only /01, /02, /03, /04, ...: don't add it to the sitemap -# if /2012 includes any files (including index.html)... add it to the sitemap -# SITEMAP_INCLUDE_FILELESS_DIRS = True - # List of files relative to the server root (!) that will be asked to be excluded # from indexing and other robotic spidering. * is supported. Will only be effective # if SITE_URL points to server root. The list is used to exclude resources from @@ -877,13 +983,14 @@ PRETTY_URLS = ${PRETTY_URLS} # Allows scheduling of posts using the rule specified here (new_post -s) # Specify an iCal Recurrence Rule: http://www.kanzaki.com/docs/ical/rrule.html # SCHEDULE_RULE = '' -# If True, use the scheduling rule to all posts by default +# If True, use the scheduling rule to all posts (not pages!) by default # SCHEDULE_ALL = False -# Do you want a add a Mathjax config file? +# Do you want to add a Mathjax config file? # MATHJAX_CONFIG = "" -# If you are using the compile-ipynb plugin, just add this one: +# If you want support for the $.$ syntax (which may conflict with running +# text!), just use this config: # MATHJAX_CONFIG = """ # <script type="text/x-mathjax-config"> # MathJax.Hub.Config({ @@ -892,7 +999,7 @@ PRETTY_URLS = ${PRETTY_URLS} # displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ], # processEscapes: true # }, -# displayAlign: 'left', // Change this to 'center' to center equations. +# displayAlign: 'center', // Change this to 'left' if you want left-aligned equations. # "HTML-CSS": { # styles: {'.MathJax_Display': {"margin": 0}} # } @@ -900,21 +1007,19 @@ PRETTY_URLS = ${PRETTY_URLS} # </script> # """ -# Want to use KaTeX instead of MathJax? While KaTeX is less featureful, -# it's faster and the output looks better. -# If you set USE_KATEX to True, you also need to add an extra CSS file -# like this: -# EXTRA_HEAD_DATA = """<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.css">""" +# Want to use KaTeX instead of MathJax? While KaTeX may not support every +# feature yet, it's faster and the output looks better. # USE_KATEX = False -# If you want to use the old (buggy) inline math $.$ with KaTeX, then -# you might want to use this feature. +# KaTeX auto-render settings. If you want support for the $.$ syntax (which may +# conflict with running text!), just use this config: # KATEX_AUTO_RENDER = """ # delimiters: [ # {left: "$$", right: "$$", display: true}, -# {left: "\\\[", right: "\\\]", display: true}, +# {left: "\\\\[", right: "\\\\]", display: true}, +# {left: "\\\\begin{equation*}", right: "\\\\end{equation*}", display: true}, # {left: "$", right: "$", display: false}, -# {left: "\\\(", right: "\\\)", display: false} +# {left: "\\\\(", right: "\\\\)", display: false} # ] # """ @@ -922,17 +1027,23 @@ PRETTY_URLS = ${PRETTY_URLS} # IPYNB_CONFIG = {} # With the following example configuration you can use a custom jinja template # called `toggle.tpl` which has to be located in your site/blog main folder: -# IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}} +# IPYNB_CONFIG = {'Exporter': {'template_file': 'toggle'}} # What Markdown extensions to enable? # You will also get gist, nikola and podcast because those are # done in the code, hope you don't mind ;-) # Note: most Nikola-specific extensions are done via the Nikola plugin system, # with the MarkdownExtension class and should not be added here. -# The default is ['fenced_code', 'codehilite'] -MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] +# Defaults are markdown.extensions.(fenced_code|codehilite|extra) +# markdown.extensions.meta is required for Markdown metadata. +MARKDOWN_EXTENSIONS = ['markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.extra'] -# Extra options to pass to the pandoc comand. +# Options to be passed to markdown extensions (See https://python-markdown.github.io/reference/) +# Default is {} (no config at all) +# MARKDOWN_EXTENSION_CONFIGS = {} + + +# Extra options to pass to the pandoc command. # by default, it's empty, is a list of strings, for example # ['-F', 'pandoc-citeproc', '--bibliography=/Users/foo/references.bib'] # Pandoc does not demote headers by default. To enable this, you can use, for example @@ -958,7 +1069,6 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] # """ # Show link to source for the posts? -# Formerly known as HIDE_SOURCELINK (inverse) # SHOW_SOURCELINK = True # Copy the source files for your pages? # Setting it to False implies SHOW_SOURCELINK = False @@ -981,22 +1091,16 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] # between each other. Old Atom feeds with no changes are marked as archived. # GENERATE_ATOM = False -# Only inlclude teasers in Atom and RSS feeds. Disabling include the full +# Only include teasers in Atom and RSS feeds. Disabling include the full # content. Defaults to True. # FEED_TEASERS = True -# Strip HTML from Atom annd RSS feed summaries and content. Defaults to False. +# Strip HTML from Atom and RSS feed summaries and content. Defaults to False. # FEED_PLAIN = False # Number of posts in Atom and RSS feeds. # FEED_LENGTH = 10 -# Include preview image as a <figure><img></figure> at the top of the entry. -# Requires FEED_PLAIN = False. If the preview image is found in the content, -# it will not be included again. Image will be included as-is, aim to optmize -# the image source for Feedly, Apple News, Flipboard, and other popular clients. -# FEED_PREVIEWIMAGE = True - # RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, # the base.tmpl will use the feed Nikola generates. However, you may want to # change it for a FeedBurner feed or something else. @@ -1081,26 +1185,52 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra'] # (Note the '.*\/' in the beginning -- matches source paths relative to conf.py) # FILE_METADATA_REGEXP = None -# If you hate "Filenames with Capital Letters and Spaces.md", you should -# set this to true. -UNSLUGIFY_TITLES = True +# Should titles fetched from file metadata be unslugified (made prettier?) +# FILE_METADATA_UNSLUGIFY_TITLES = True + +# If enabled, extract metadata from docinfo fields in reST documents. +# If your text files start with a level 1 heading, it will be treated as the +# document title and will be removed from the text. +# USE_REST_DOCINFO_METADATA = False + +# If enabled, hide docinfo fields in reST document output +# HIDE_REST_DOCINFO = False + +# Map metadata from other formats to Nikola names. +# Supported formats: ${_METADATA_MAPPING_FORMATS} +# METADATA_MAPPING = {} +# +# Example for Pelican compatibility: +# METADATA_MAPPING = { +# "rest_docinfo": {"summary": "description", "modified": "updated"}, +# "markdown_metadata": {"summary": "description", "modified": "updated"} +# } +# Other examples: https://getnikola.com/handbook.html#mapping-metadata-from-other-formats + +# Map metadata between types/values. (Runs after METADATA_MAPPING.) +# Supported formats: nikola, ${_METADATA_MAPPING_FORMATS} +# The value on the right should be a dict of callables. +# METADATA_VALUE_MAPPING = {} +# Examples: +# METADATA_VALUE_MAPPING = { +# "yaml": {"keywords": lambda value: ', '.join(value)}, # yaml: 'keywords' list -> str +# "nikola": { +# "widgets": lambda value: value.split(', '), # nikola: 'widgets' comma-separated string -> list +# "tags": str.lower # nikola: force lowercase 'tags' (input would be string) +# } +# } + +# Add any post types here that you want to be displayed without a title. +# If your theme supports it, the titles will not be shown. +# TYPES_TO_HIDE_TITLE = [] # Additional metadata that is added to a post when creating a new_post # ADDITIONAL_METADATA = {} -# Nikola supports Open Graph Protocol data for enhancing link sharing and -# discoverability of your site on Facebook, Google+, and other services. -# Open Graph is enabled by default. -# USE_OPEN_GRAPH = True - # Nikola supports Twitter Card summaries, but they are disabled by default. # They make it possible for you to attach media to Tweets that link # to your content. # -# IMPORTANT: -# Please note, that you need to opt-in for using Twitter Cards! -# To do this please visit https://cards-dev.twitter.com/validator -# # Uncomment and modify to following lines to match your accounts. # Images displayed come from the `previewimage` meta tag. # You can specify the card type by using the `card` parameter in TWITTER_CARD. @@ -1112,14 +1242,20 @@ UNSLUGIFY_TITLES = True # # 'creator': '@username', # Username for the content creator / author. # } -# If webassets is installed, bundle JS and CSS into single files to make -# site loading faster in a HTTP/1.1 environment but is not recommended for -# HTTP/2.0 when caching is used. Defaults to True. +# Bundle JS and CSS into single files to make site loading faster in a HTTP/1.1 +# environment but is not recommended for HTTP/2.0 when caching is used. +# Defaults to True. # USE_BUNDLES = True # Plugins you don't want to use. Be careful :-) # DISABLED_PLUGINS = ["render_galleries"] +# Special settings to disable only parts of the indexes plugin. +# Use with care. +# DISABLE_INDEXES = False +# DISABLE_MAIN_ATOM_FEED = False +# DISABLE_MAIN_RSS_FEED = False + # Add the absolute paths to directories containing plugins to use them. # For example, the `plugins` directory of your clone of the Nikola plugins # repository. @@ -1147,18 +1283,21 @@ UNSLUGIFY_TITLES = True # (defaults to 1.) # DEMOTE_HEADERS = 1 -# Docutils, by default, will perform a transform in your documents -# extracting unique titles at the top of your document and turning -# them into metadata. This surprises a lot of people, and setting -# this option to True will prevent it. -# NO_DOCUTILS_TITLE_TRANSFORM = False - # If you don’t like slugified file names ([a-z0-9] and a literal dash), # and would prefer to use all the characters your file system allows. # USE WITH CARE! This is also not guaranteed to be perfect, and may # sometimes crash Nikola, your web server, or eat your cat. # USE_SLUGIFY = True +# If set to True, the tags 'draft', 'mathjax' and 'private' have special +# meaning. If set to False, these tags are handled like regular tags. +USE_TAG_METADATA = False + +# If set to True, a warning is issued if one of the 'draft', 'mathjax' +# and 'private' tags are found in a post. Useful for checking that +# migration was successful. +WARN_ABOUT_TAG_METADATA = False + # 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/galleries/demo/metadata.sample.yml b/nikola/data/samplesite/galleries/demo/metadata.sample.yml new file mode 100644 index 0000000..f504573 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/metadata.sample.yml @@ -0,0 +1,13 @@ +--- +name: tesla_tower1_lg.jpg +caption: Wardenclyffe Tower +built_in: 1904 +order: 2 +--- +name: tesla4_lg.jpg +order: 0 +--- +name: tesla_conducts_lg.jpg +caption: Nikola Tesla conducts electricity +order: 1 +--- diff --git a/nikola/data/samplesite/listings/hello.py b/nikola/data/samplesite/listings/hello.py index 885acde..5535df8 100644 --- a/nikola/data/samplesite/listings/hello.py +++ b/nikola/data/samplesite/listings/hello.py @@ -7,5 +7,6 @@ def hello(name='world'): greeting = "hello " + name print(greeting) + if __name__ == "__main__": hello(*sys.argv[1:]) diff --git a/nikola/data/samplesite/pages/bootstrap-demo.rst b/nikola/data/samplesite/pages/bootstrap-demo.rst index 481140a..35a0265 100644 --- a/nikola/data/samplesite/pages/bootstrap-demo.rst +++ b/nikola/data/samplesite/pages/bootstrap-demo.rst @@ -357,7 +357,7 @@ </blockquote> </div> <div class="col-lg-6"> - <blockquote class="pull-right"> + <blockquote class="float-md-right"> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p> <small>Someone famous in <cite title="Source Title">Source Title</cite></small> </blockquote> diff --git a/nikola/data/samplesite/pages/charts.txt b/nikola/data/samplesite/pages/charts.rst index 72fedb1..72fedb1 100644 --- a/nikola/data/samplesite/pages/charts.txt +++ b/nikola/data/samplesite/pages/charts.rst diff --git a/nikola/data/samplesite/pages/creating-a-theme.rst b/nikola/data/samplesite/pages/creating-a-theme.rst index 108a192..66d75d1 120000 --- a/nikola/data/samplesite/pages/creating-a-theme.rst +++ b/nikola/data/samplesite/pages/creating-a-theme.rst @@ -1 +1 @@ -../../../../docs/creating-a-theme.txt
\ No newline at end of file +../../../../docs/creating-a-theme.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst b/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst index 6175355..9342f11 100644 --- a/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst +++ b/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst @@ -1,468 +1,468 @@ -.. title: A BID FOR FORTUNE OR; DR. NIKOLA'S VENDETTA
-.. template: book.tmpl
-.. hyphenate: yes
-.. filters: filters.typogrify
-
-.. class:: subtitle
-
-By `GUY BOOTHBY <http://www.gutenberg.org/ebooks/author/3587>`__
-
-Author of "Dr. Nikola," "The Beautiful White Devil," etc., etc.
-
-.. figure:: /images/frontispiece.jpg
- :class: bookfig
-
-.. topic:: The Project Gutenberg EBook of A Bid for Fortune, by Guy Boothby
-
- This eBook is for the use of anyone anywhere at no cost and with
- almost no restrictions whatsoever. You may copy it, give it away or
- re-use it under the terms of the Project Gutenberg License included
- with this eBook or online at www.gutenberg.org
-
-
- Title: A Bid for Fortune
- or Dr. Nikola's Vendetta
-
- Author: `Guy Boothby <http://www.gutenberg.org/ebooks/author/3587>`__
-
- Release Date: May 29, 2007 [EBook #21640]
-
- Language: English
-
- Produced by Marilynda Fraser-Cunliffe, Mary Meehan and the
- Online Distributed Proofreading Team at http://www.pgdp.net
-
- Originally published by:
-
- WARD, LOCK & CO., LIMITED
- LONDON, MELBOURNE AND TORONTO
- 1918
-
-.. figure:: /images/illus_001.jpg
- :class: bookfig
-
-PART I
-======
-
-PROLOGUE
---------
-
-.. role:: smallcaps
-
-
-:smallcaps:`The` manager of the new Imperial Restaurant on the Thames Embankment went
-into his luxurious private office and shut the door. Having done so, he
-first scratched his chin reflectively, and then took a letter from the
-drawer in which it had reposed for more than two months and perused it
-carefully. Though he was not aware of it, this was the thirtieth time he
-had read it since breakfast that morning. And yet he was not a whit
-nearer understanding it than he had been at the beginning. He turned it
-over and scrutinized the back, where not a sign of writing was to be
-seen; he held it up to the window, as if he might hope to discover
-something from the water-mark; but there was nothing in either of these
-places of a nature calculated to set his troubled mind at rest. Then he
-took a magnificent repeater watch from his waistcoat pocket and glanced
-at the dial; the hands stood at half-past seven. He immediately threw
-the letter on the table, and as he did so his anxiety found relief in
-words.
-
-"It's really the most extraordinary affair I ever had to do with," he
-remarked. "And as I've been in the business just three-and-thirty years
-at eleven a.m. next Monday morning, I ought to know something about it.
-I only hope I've done right, that's all."
-
-As he spoke, the chief bookkeeper, who had the treble advantage of being
-tall, pretty, and just eight-and-twenty years of age, entered the room.
-She noticed the open letter and the look upon her chief's face, and her
-curiosity was proportionately excited.
-
-"You seem worried, Mr. McPherson," she said tenderly, as she put down
-the papers she had brought in for his signature.
-
-"You have just hit it, Miss O'Sullivan," he answered, pushing them
-farther on to the table. "I am worried about many things, but
-particularly about this letter."
-
-He handed the epistle to her, and she, being desirous of impressing him
-with her business capabilities, read it with ostentatious care. But it
-was noticeable that when she reached the signature she too turned back
-to the beginning, and then deliberately read it over again. The manager
-rose, crossed to the mantelpiece, and rang for the head waiter. Having
-relieved his feelings in this way, he seated himself again at his
-writing-table, put on his glasses, and stared at his companion, while
-waiting for her to speak.
-
-"It's very funny," she said. "Very funny indeed!"
-
-"It's the most extraordinary communication I have ever received," he
-replied with conviction. "You see it is written from Cuyaba, Brazil. The
-date is three months ago to a day. Now I have taken the trouble to find
-out where and what Cuyaba is."
-
-He made this confession with an air of conscious pride, and having done
-so, laid himself back in his chair, stuck his thumbs into the armholes
-of his waistcoat, and looked at his fair subordinate for approval. Nor
-was he destined to be disappointed. He was a bachelor in possession of a
-snug income, and she, besides being pretty, was a lady with a keen eye
-to the main chance.
-
-"And where *is* Cuyaba?" she asked humbly.
-
-"Cuyaba," he replied, rolling his tongue with considerable relish round
-his unconscious mispronunciation of the name, "is a town almost on the
-western or Bolivian border of Brazil. It is of moderate size, is
-situated on the banks of the river Cuyaba, and is considerably connected
-with the famous Brazilian Diamond Fields."
-
-"And does the writer of this letter live there?"
-
-"I cannot say. He writes from there--that is enough for us."
-
-"And he orders dinner for four--here, in a private room overlooking the
-river, three months ahead--punctually at eight o'clock, gives you a list
-of the things he wants, and even arranges the decoration of the table.
-Says he has never seen either of his three friends before; that one of
-them hails from (here she consulted the letter again) Hang-chow, another
-from Bloemfontein, while the third resides, at present, in England. Each
-one is to present an ordinary visiting card with a red dot on it to the
-porter in the hall, and to be shown to the room at once. I don't
-understand it at all."
-
-The manager paused for a moment, and then said deliberately,--"Hang-chow
-is in China, Bloemfontein is in South Africa."
-
-"What a wonderful man you are, to be sure, Mr. McPherson! I never can
-*think* how you manage to carry so much in your head."
-
-There spoke the true woman. And it was a move in the right direction,
-for the manager was susceptible to her gentle influence, as she had
-occasion to know.
-
-At this juncture the head waiter appeared upon the scene, and took up a
-position just inside the doorway, as if he were afraid of injuring the
-carpet by coming farther.
-
-"Is No. 22 ready, Williams?"
-
-"Quite ready, sir. The wine is on the ice, and cook tells me he'll be
-ready to dish punctual to the moment."
-
-"The letter says, 'no electric light; candles with red shades.' Have you
-put on those shades I got this morning?"
-
-"Just seen it done this very minute, sir."
-
-"And let me see, there was one other thing." He took the letter from the
-chief bookkeeper's hand and glanced at it. "Ah, yes, a porcelain saucer,
-and a small jug of new milk upon the mantelpiece. An extraordinary
-request, but has it been attended to?"
-
-"I put it there myself, sir."
-
-"Who wait?"
-
-"Jones, Edmunds, Brooks, and Tomkins."
-
-"Very good. Then I think that will do. Stay! You had better tell the
-hall porter to look out for three gentlemen presenting plain visiting
-cards with a little red spot on them. Let Brooks wait in the hall, and
-when they arrive tell him to show them straight up to the room."
-
-"It shall be done, sir."
-
-The head waiter left the room, and the manager stretched himself in his
-chair, yawned by way of showing his importance, and then said
-solemnly,--
-
-"I don't believe they'll any of them turn up; but if they do, this Dr.
-Nikola, whoever he may be, won't be able to find fault with my
-arrangements."
-
-Then, leaving the dusty high road of Business, he and his companion
-wandered in the shady bridle-paths of Love--to the end that when the
-chief bookkeeper returned to her own department she had forgotten the
-strange dinner party about to take place upstairs, and was busily
-engaged upon a calculation as to how she would look in white satin and
-orange blossoms, and, that settled, fell to wondering whether it was
-true, as Miss Joyce, a subordinate, had been heard to declare, that the
-manager had once shown himself partial to a certain widow with reputed
-savings and a share in an extensive egg and dairy business.
-
-At ten minutes to eight precisely a hansom drew up at the steps of the
-hotel. As soon as it stopped, an undersized gentleman, with a clean
-shaven countenance, a canonical corporation, and bow legs, dressed in a
-decidedly clerical garb, alighted. He paid and discharged his cabman,
-and then took from his ticket pocket an ordinary white visiting card,
-which he presented to the gold-laced individual who had opened the
-apron. The latter, having noted the red spot, called a waiter, and the
-reverend gentleman was immediately escorted upstairs.
-
-Hardly had the attendant time to return to his station in the hall,
-before a second cab made its appearance, closely followed by a third.
-Out of the second jumped a tall, active, well-built man of about thirty
-years of age. He was dressed in evening dress of the latest fashion, and
-to conceal it from the vulgar gaze, wore a large Inverness cape of heavy
-texture. He also in his turn handed a white card to the porter, and,
-having done so, proceeded into the hall, followed by the occupant of the
-last cab, who had closely copied his example. This individual was also
-in evening dress, but it was of a different stamp. It was old-fashioned
-and had seen much use. The wearer, too, was taller than the ordinary run
-of men, while it was noticeable that his hair was snow-white, and that
-his face was deeply pitted with smallpox. After disposing of their hats
-and coats in an ante-room, they reached room No. 22, where they found
-the gentleman in clerical costume pacing impatiently up and down.
-
-Left alone, the tallest of the trio, who for want of a better title we
-may call the Best Dressed Man, took out his watch, and having glanced at
-it, looked at his companions. "Gentlemen," he said, with a slight
-American accent, "it is three minutes to eight o'clock. My name is
-Eastover!"
-
-"I'm glad to hear it, for I'm most uncommonly hungry," said the next
-tallest, whom I have already described as being so marked by disease.
-"My name is Prendergast!"
-
-"We only wait for our friend and host," remarked the clerical gentleman,
-as if he felt he ought to take a share in the conversation, and then, as
-an afterthought, he continued, "My name is Baxter!"
-
-They shook hands all round with marked cordiality, seated themselves
-again, and took it in turns to examine the clock.
-
-"Have you ever had the pleasure of meeting our host before?" asked Mr.
-Baxter of Mr. Prendergast.
-
-"Never," replied that gentleman, with a shake of his head. "Perhaps Mr.
-Eastover has been more fortunate?"
-
-"Not I," was the brief rejoinder. "I've had to do with him off and on
-for longer than I care to reckon, but I've never set eyes on him up to
-date."
-
-"And where may he have been the first time you heard from him?"
-
-"In Nashville, Tennessee," said Eastover. "After that, Tahupapa, New
-Zealand; after that, Papeete, in the Society Islands; then Pekin, China.
-And you?"
-
-"First time, Brussels; second, Monte Video; third, Mandalay, and then
-the Gold Coast, Africa. It's your turn, Mr. Baxter."
-
-The clergyman glanced at the timepiece. It was exactly eight o'clock.
-"First time, Cabul, Afghanistan; second, Nijni Novgorod, Russia; third,
-Wilcannia, Darling River, Australia; fourth, Valparaiso, Chili; fifth,
-Nagasaki, Japan."
-
-"He is evidently a great traveller and a most mysterious person."
-
-"He is more than that," said Eastover with conviction; "he is late for
-dinner!"
-
-Prendergast looked at his watch.
-
-"That clock is two minutes fast. Hark, there goes Big Ben! Eight
-exactly."
-
-As he spoke the door was thrown open and a voice announced "Dr. Nikola."
-
-The three men sprang to their feet simultaneously, with exclamations of
-astonishment, as the man they had been discussing made his appearance.
-
-It would take more time than I can spare the subject to give you an
-adequate and inclusive description of the person who entered the room at
-that moment. In stature he was slightly above the ordinary, his
-shoulders were broad, his limbs perfectly shaped and plainly muscular,
-but very slim. His head, which was magnificently set upon his shoulders,
-was adorned with a profusion of glossy black hair; his face was
-destitute of beard or moustache, and was of oval shape and handsome
-moulding; while his skin was of a dark olive hue, a colour which
-harmonized well with his piercing black eyes and pearly teeth. His hands
-and feet were small, and the greatest dandy must have admitted that he
-was irreproachably dressed, with a neatness that bordered on the
-puritanical. In age he might have been anything from eight-and-twenty to
-forty; in reality he was thirty-three. He advanced into the room and
-walked with out-stretched hand directly across to where Eastover was
-standing by the fireplace.
-
-"Mr. Eastover, I feel certain," he said, fixing his glittering eyes upon
-the man he addressed, and allowing a curious smile to play upon his
-face.
-
-"That is my name, Dr. Nikola," the other answered with evident surprise.
-"But how on earth can you distinguish me from your other guests?"
-
-"Ah! it would surprise you if you knew. And Mr. Prendergast, and Mr.
-Baxter. This is delightful; I hope I am not late. We had a collision in
-the Channel this morning, and I was almost afraid I might not be up to
-time. Dinner seems ready; shall we sit down to it?" They seated
-themselves, and the meal commenced. The Imperial Restaurant has earned
-an enviable reputation for doing things well, and the dinner that night
-did not in any way detract from its lustre. But, delightful as it all
-was, it was noticeable that the three guests paid more attention to
-their host than to his excellent *menu*. As they had said before his
-arrival, they had all had dealings with him for several years, but what
-those dealings were they were careful not to describe. It was more than
-possible that they hardly liked to remember them themselves.
-
-When coffee had been served and the servants had withdrawn, Dr. Nikola
-rose from the table, and went across to the massive sideboard. On it
-stood a basket of very curious shape and workmanship. This he opened,
-and as he did so, to the astonishment of his guests, an enormous cat, as
-black as his master's coat, leaped out on to the floor. The reason for
-the saucer and jug of milk became evident.
-
-Seating himself at the table again, the host followed the example of his
-guests and lit a cigar, blowing a cloud of smoke luxuriously through his
-delicately chiselled nostrils. His eyes wandered round the cornice of
-the room, took in the pictures and decorations, and then came down to
-meet the faces of his companions. As they did so, the black cat, having
-finished its meal, sprang on to his shoulder to crouch there, watching
-the three men through the curling smoke drift with its green blinking,
-fiendish eyes. Dr. Nikola smiled as he noticed the effect the animal had
-upon his guests.
-
-"Now shall we get to business?" he said briskly.
-
-The others almost simultaneously knocked the ashes off their cigars and
-brought themselves to attention. Dr. Nikola's dainty, languid manner
-seemed to drop from him like a cloak, his eyes brightened, and his
-voice, when he spoke, was clean cut as chiselled silver.
-
-"You are doubtless anxious to be informed why I summoned you from all
-parts of the globe to meet me here to-night? And it is very natural you
-should be. But then, from what you know of me, you should not be
-surprised at anything I do."
-
-His voice dropped back into its old tone of gentle languor. He drew in a
-great breath of smoke and then sent it slowly out from his lips again.
-His eyes were half closed, and he drummed with one finger on the table
-edge. The cat looked through the smoke at the three men, and it seemed
-to them that he grew every moment larger and more ferocious. Presently
-his owner took him from his perch, and seating him on his knee fell to
-stroking his fur, from head to tail, with his long slim fingers. It was
-as if he were drawing inspiration for some deadly mischief from the
-uncanny beast.
-
-"To preface what I have to say to you, let me tell you that this is by
-far the most important business for which I have ever required your
-help. (Three slow strokes down the centre of the back, and one round
-each ear.) When it first came into my mind I was at a loss who to trust
-in the matter. I thought of Vendon, but I found Vendon was dead. I
-thought of Brownlow, but Brownlow was no longer faithful. (Two strokes
-down the back and two on the throat.) Then bit by bit I remembered you.
-I was in Brazil at the time. So I sent for you. You came. So far so
-good."
-
-He rose, and crossed over to the fireplace. As he went the cat crawled
-back to its original position on his shoulder. Then his voice changed
-once more to its former business-like tone.
-
-"I am not going to tell you very much about it. But from what I do tell
-you, you will be able to gather a great deal and imagine the rest. To
-begin with, there is a man living in this world to-day who has done me a
-great and lasting injury. What that injury is is no concern of yours.
-You would not understand if I told you. So we'll leave that out of the
-question. He is immensely rich. His cheque for £300,000 would be
-honoured by his bank at any minute. Obviously he is a power. He has had
-reason to know that I am pitting my wits against his, and he flatters
-himself that so far he has got the better of me. That is because I am
-drawing him on. I am maturing a plan which will make him a poor and a
-very miserable man at one and the same time. If that scheme succeeds,
-and I am satisfied with the way you three men have performed the parts I
-shall call on you to play in it, I shall pay to each of you the sum of
-£10,000. If it doesn't succeed, then you will each receive a thousand
-and your expenses. Do you follow me?"
-
-It was evident from their faces that they hung upon his every word.
-
-"But, remember, I demand from you your whole and entire labour. While
-you are serving me you are mine body and soul. I know you are
-trustworthy. I have had good proof that you are--pardon the
-expression--unscrupulous, and I flatter myself you are silent. What is
-more, I shall tell you nothing beyond what is necessary for the carrying
-out of my scheme, so that you could not betray me if you would. Now for
-my plans!"
-
-He sat down again and took a paper from his pocket. Having perused it,
-he turned to Eastover.
-
-"You will leave at once--that is to say, by the boat on Wednesday--for
-Sydney. You will book your passage to-morrow morning, first thing, and
-join her in Plymouth. You will meet me to-morrow evening at an address I
-will send you, and receive your final instructions. Good-night."
-
-Seeing that he was expected to go, Eastover rose, shook hands, and left
-the room without a word. He was too astonished to hesitate or to say
-anything.
-
-Nikola took another letter from his pocket and turned to Prendergast.
-"*You* will go down to Dover to-night, cross to Paris to-morrow morning,
-and leave this letter personally at the address you will find written on
-it. On Thursday, at half-past two precisely, you will deliver me an
-answer in the porch at Charing Cross. You will find sufficient money in
-that envelope to pay all your expenses. Now go!"
-
-"At half-past two you shall have your answer. Good-night."
-
-"Good-night."
-
-When Prendergast had left the room, Dr. Nikola lit another cigar and
-turned his attentions to Mr. Baxter.
-
-"Six months ago, Mr. Baxter, I found for you a situation as tutor to the
-young Marquis of Beckenham. You still hold it, I suppose?"
-
-"I do."
-
-"Is the father well disposed towards you?"
-
-"In every way. I have done my best to ingratiate myself with him. That
-was one of your instructions."
-
-"Yes, yes! But I was not certain that you would succeed. If the old man
-is anything like what he was when I last met him he must still be a
-difficult person to deal with. Does the boy like you?"
-
-"I hope so."
-
-"Have you brought me his photograph as I directed?"
-
-"I have. Here it is."
-
-Baxter took a photograph from his pocket and handed it across the table.
-
-"Good. You have done very well, Mr. Baxter. I am pleased with you.
-To-morrow morning you will go back to Yorkshire----"
-
-"I beg your pardon, Bournemouth. His Grace owns a house near
-Bournemouth, which he occupies during the summer months."
-
-"Very well--then to-morrow morning you will go back to Bournemouth and
-continue to ingratiate yourself with father and son. You will also begin
-to implant in the boy's mind a desire for travel. Don't let him become
-aware that his desire has its source in you--but do not fail to foster
-it all you can. I will communicate with you further in a day or two. Now
-go."
-
-Baxter in his turn left the room. The door closed. Dr. Nikola picked up
-the photograph and studied it.
-
-"The likeness is unmistakable--or it ought to be. My friend, my very
-dear friend, Wetherell, my toils are closing on you. My arrangements are
-perfecting themselves admirably. Presently, when all is complete, I
-shall press the lever, the machinery will be set in motion, and you will
-find yourself being slowly but surely ground into powder. Then you will
-hand over what I want, and be sorry you thought fit to baulk Dr.
-Nikola!"
-
-He rang the bell and ordered his bill. This duty discharged, he placed
-the cat back in its prison, shut the lid, descended with the basket to
-the hall, and called a hansom. The porter inquired to what address he
-should order the cabman to drive. Dr. Nikola did not reply for a moment,
-then he said, as if he had been thinking something out: "The *Green
-Sailor* public-house, East India Dock Road."
-
-
-------------------------
-
-You can read the rest of "A Bid For Fortune; Or, Dr. Nikola's Vendetta" at `Open Library <https://archive.org/stream/bidforfortunenov00bootiala#page/12/mode/2up>`__
+.. title: A BID FOR FORTUNE OR; DR. NIKOLA'S VENDETTA +.. template: book.tmpl +.. hyphenate: yes +.. filters: filters.typogrify + +.. class:: subtitle + +By `GUY BOOTHBY <http://www.gutenberg.org/ebooks/author/3587>`__ + +Author of "Dr. Nikola," "The Beautiful White Devil," etc., etc. + +.. figure:: /images/frontispiece.jpg + :class: bookfig + +.. topic:: The Project Gutenberg EBook of A Bid for Fortune, by Guy Boothby + + This eBook is for the use of anyone anywhere at no cost and with + almost no restrictions whatsoever. You may copy it, give it away or + re-use it under the terms of the Project Gutenberg License included + with this eBook or online at www.gutenberg.org + + + Title: A Bid for Fortune + or Dr. Nikola's Vendetta + + Author: `Guy Boothby <http://www.gutenberg.org/ebooks/author/3587>`__ + + Release Date: May 29, 2007 [EBook #21640] + + Language: English + + Produced by Marilynda Fraser-Cunliffe, Mary Meehan and the + Online Distributed Proofreading Team at http://www.pgdp.net + + Originally published by: + + WARD, LOCK & CO., LIMITED + LONDON, MELBOURNE AND TORONTO + 1918 + +.. figure:: /images/illus_001.jpg + :class: bookfig + +PART I +====== + +PROLOGUE +-------- + +.. role:: smallcaps + + +:smallcaps:`The` manager of the new Imperial Restaurant on the Thames Embankment went +into his luxurious private office and shut the door. Having done so, he +first scratched his chin reflectively, and then took a letter from the +drawer in which it had reposed for more than two months and perused it +carefully. Though he was not aware of it, this was the thirtieth time he +had read it since breakfast that morning. And yet he was not a whit +nearer understanding it than he had been at the beginning. He turned it +over and scrutinized the back, where not a sign of writing was to be +seen; he held it up to the window, as if he might hope to discover +something from the water-mark; but there was nothing in either of these +places of a nature calculated to set his troubled mind at rest. Then he +took a magnificent repeater watch from his waistcoat pocket and glanced +at the dial; the hands stood at half-past seven. He immediately threw +the letter on the table, and as he did so his anxiety found relief in +words. + +"It's really the most extraordinary affair I ever had to do with," he +remarked. "And as I've been in the business just three-and-thirty years +at eleven a.m. next Monday morning, I ought to know something about it. +I only hope I've done right, that's all." + +As he spoke, the chief bookkeeper, who had the treble advantage of being +tall, pretty, and just eight-and-twenty years of age, entered the room. +She noticed the open letter and the look upon her chief's face, and her +curiosity was proportionately excited. + +"You seem worried, Mr. McPherson," she said tenderly, as she put down +the papers she had brought in for his signature. + +"You have just hit it, Miss O'Sullivan," he answered, pushing them +farther on to the table. "I am worried about many things, but +particularly about this letter." + +He handed the epistle to her, and she, being desirous of impressing him +with her business capabilities, read it with ostentatious care. But it +was noticeable that when she reached the signature she too turned back +to the beginning, and then deliberately read it over again. The manager +rose, crossed to the mantelpiece, and rang for the head waiter. Having +relieved his feelings in this way, he seated himself again at his +writing-table, put on his glasses, and stared at his companion, while +waiting for her to speak. + +"It's very funny," she said. "Very funny indeed!" + +"It's the most extraordinary communication I have ever received," he +replied with conviction. "You see it is written from Cuyaba, Brazil. The +date is three months ago to a day. Now I have taken the trouble to find +out where and what Cuyaba is." + +He made this confession with an air of conscious pride, and having done +so, laid himself back in his chair, stuck his thumbs into the armholes +of his waistcoat, and looked at his fair subordinate for approval. Nor +was he destined to be disappointed. He was a bachelor in possession of a +snug income, and she, besides being pretty, was a lady with a keen eye +to the main chance. + +"And where *is* Cuyaba?" she asked humbly. + +"Cuyaba," he replied, rolling his tongue with considerable relish round +his unconscious mispronunciation of the name, "is a town almost on the +western or Bolivian border of Brazil. It is of moderate size, is +situated on the banks of the river Cuyaba, and is considerably connected +with the famous Brazilian Diamond Fields." + +"And does the writer of this letter live there?" + +"I cannot say. He writes from there--that is enough for us." + +"And he orders dinner for four--here, in a private room overlooking the +river, three months ahead--punctually at eight o'clock, gives you a list +of the things he wants, and even arranges the decoration of the table. +Says he has never seen either of his three friends before; that one of +them hails from (here she consulted the letter again) Hang-chow, another +from Bloemfontein, while the third resides, at present, in England. Each +one is to present an ordinary visiting card with a red dot on it to the +porter in the hall, and to be shown to the room at once. I don't +understand it at all." + +The manager paused for a moment, and then said deliberately,--"Hang-chow +is in China, Bloemfontein is in South Africa." + +"What a wonderful man you are, to be sure, Mr. McPherson! I never can +*think* how you manage to carry so much in your head." + +There spoke the true woman. And it was a move in the right direction, +for the manager was susceptible to her gentle influence, as she had +occasion to know. + +At this juncture the head waiter appeared upon the scene, and took up a +position just inside the doorway, as if he were afraid of injuring the +carpet by coming farther. + +"Is No. 22 ready, Williams?" + +"Quite ready, sir. The wine is on the ice, and cook tells me he'll be +ready to dish punctual to the moment." + +"The letter says, 'no electric light; candles with red shades.' Have you +put on those shades I got this morning?" + +"Just seen it done this very minute, sir." + +"And let me see, there was one other thing." He took the letter from the +chief bookkeeper's hand and glanced at it. "Ah, yes, a porcelain saucer, +and a small jug of new milk upon the mantelpiece. An extraordinary +request, but has it been attended to?" + +"I put it there myself, sir." + +"Who wait?" + +"Jones, Edmunds, Brooks, and Tomkins." + +"Very good. Then I think that will do. Stay! You had better tell the +hall porter to look out for three gentlemen presenting plain visiting +cards with a little red spot on them. Let Brooks wait in the hall, and +when they arrive tell him to show them straight up to the room." + +"It shall be done, sir." + +The head waiter left the room, and the manager stretched himself in his +chair, yawned by way of showing his importance, and then said +solemnly,-- + +"I don't believe they'll any of them turn up; but if they do, this Dr. +Nikola, whoever he may be, won't be able to find fault with my +arrangements." + +Then, leaving the dusty high road of Business, he and his companion +wandered in the shady bridle-paths of Love--to the end that when the +chief bookkeeper returned to her own department she had forgotten the +strange dinner party about to take place upstairs, and was busily +engaged upon a calculation as to how she would look in white satin and +orange blossoms, and, that settled, fell to wondering whether it was +true, as Miss Joyce, a subordinate, had been heard to declare, that the +manager had once shown himself partial to a certain widow with reputed +savings and a share in an extensive egg and dairy business. + +At ten minutes to eight precisely a hansom drew up at the steps of the +hotel. As soon as it stopped, an undersized gentleman, with a clean +shaven countenance, a canonical corporation, and bow legs, dressed in a +decidedly clerical garb, alighted. He paid and discharged his cabman, +and then took from his ticket pocket an ordinary white visiting card, +which he presented to the gold-laced individual who had opened the +apron. The latter, having noted the red spot, called a waiter, and the +reverend gentleman was immediately escorted upstairs. + +Hardly had the attendant time to return to his station in the hall, +before a second cab made its appearance, closely followed by a third. +Out of the second jumped a tall, active, well-built man of about thirty +years of age. He was dressed in evening dress of the latest fashion, and +to conceal it from the vulgar gaze, wore a large Inverness cape of heavy +texture. He also in his turn handed a white card to the porter, and, +having done so, proceeded into the hall, followed by the occupant of the +last cab, who had closely copied his example. This individual was also +in evening dress, but it was of a different stamp. It was old-fashioned +and had seen much use. The wearer, too, was taller than the ordinary run +of men, while it was noticeable that his hair was snow-white, and that +his face was deeply pitted with smallpox. After disposing of their hats +and coats in an ante-room, they reached room No. 22, where they found +the gentleman in clerical costume pacing impatiently up and down. + +Left alone, the tallest of the trio, who for want of a better title we +may call the Best Dressed Man, took out his watch, and having glanced at +it, looked at his companions. "Gentlemen," he said, with a slight +American accent, "it is three minutes to eight o'clock. My name is +Eastover!" + +"I'm glad to hear it, for I'm most uncommonly hungry," said the next +tallest, whom I have already described as being so marked by disease. +"My name is Prendergast!" + +"We only wait for our friend and host," remarked the clerical gentleman, +as if he felt he ought to take a share in the conversation, and then, as +an afterthought, he continued, "My name is Baxter!" + +They shook hands all round with marked cordiality, seated themselves +again, and took it in turns to examine the clock. + +"Have you ever had the pleasure of meeting our host before?" asked Mr. +Baxter of Mr. Prendergast. + +"Never," replied that gentleman, with a shake of his head. "Perhaps Mr. +Eastover has been more fortunate?" + +"Not I," was the brief rejoinder. "I've had to do with him off and on +for longer than I care to reckon, but I've never set eyes on him up to +date." + +"And where may he have been the first time you heard from him?" + +"In Nashville, Tennessee," said Eastover. "After that, Tahupapa, New +Zealand; after that, Papeete, in the Society Islands; then Pekin, China. +And you?" + +"First time, Brussels; second, Monte Video; third, Mandalay, and then +the Gold Coast, Africa. It's your turn, Mr. Baxter." + +The clergyman glanced at the timepiece. It was exactly eight o'clock. +"First time, Cabul, Afghanistan; second, Nijni Novgorod, Russia; third, +Wilcannia, Darling River, Australia; fourth, Valparaiso, Chili; fifth, +Nagasaki, Japan." + +"He is evidently a great traveller and a most mysterious person." + +"He is more than that," said Eastover with conviction; "he is late for +dinner!" + +Prendergast looked at his watch. + +"That clock is two minutes fast. Hark, there goes Big Ben! Eight +exactly." + +As he spoke the door was thrown open and a voice announced "Dr. Nikola." + +The three men sprang to their feet simultaneously, with exclamations of +astonishment, as the man they had been discussing made his appearance. + +It would take more time than I can spare the subject to give you an +adequate and inclusive description of the person who entered the room at +that moment. In stature he was slightly above the ordinary, his +shoulders were broad, his limbs perfectly shaped and plainly muscular, +but very slim. His head, which was magnificently set upon his shoulders, +was adorned with a profusion of glossy black hair; his face was +destitute of beard or moustache, and was of oval shape and handsome +moulding; while his skin was of a dark olive hue, a colour which +harmonized well with his piercing black eyes and pearly teeth. His hands +and feet were small, and the greatest dandy must have admitted that he +was irreproachably dressed, with a neatness that bordered on the +puritanical. In age he might have been anything from eight-and-twenty to +forty; in reality he was thirty-three. He advanced into the room and +walked with out-stretched hand directly across to where Eastover was +standing by the fireplace. + +"Mr. Eastover, I feel certain," he said, fixing his glittering eyes upon +the man he addressed, and allowing a curious smile to play upon his +face. + +"That is my name, Dr. Nikola," the other answered with evident surprise. +"But how on earth can you distinguish me from your other guests?" + +"Ah! it would surprise you if you knew. And Mr. Prendergast, and Mr. +Baxter. This is delightful; I hope I am not late. We had a collision in +the Channel this morning, and I was almost afraid I might not be up to +time. Dinner seems ready; shall we sit down to it?" They seated +themselves, and the meal commenced. The Imperial Restaurant has earned +an enviable reputation for doing things well, and the dinner that night +did not in any way detract from its lustre. But, delightful as it all +was, it was noticeable that the three guests paid more attention to +their host than to his excellent *menu*. As they had said before his +arrival, they had all had dealings with him for several years, but what +those dealings were they were careful not to describe. It was more than +possible that they hardly liked to remember them themselves. + +When coffee had been served and the servants had withdrawn, Dr. Nikola +rose from the table, and went across to the massive sideboard. On it +stood a basket of very curious shape and workmanship. This he opened, +and as he did so, to the astonishment of his guests, an enormous cat, as +black as his master's coat, leaped out on to the floor. The reason for +the saucer and jug of milk became evident. + +Seating himself at the table again, the host followed the example of his +guests and lit a cigar, blowing a cloud of smoke luxuriously through his +delicately chiselled nostrils. His eyes wandered round the cornice of +the room, took in the pictures and decorations, and then came down to +meet the faces of his companions. As they did so, the black cat, having +finished its meal, sprang on to his shoulder to crouch there, watching +the three men through the curling smoke drift with its green blinking, +fiendish eyes. Dr. Nikola smiled as he noticed the effect the animal had +upon his guests. + +"Now shall we get to business?" he said briskly. + +The others almost simultaneously knocked the ashes off their cigars and +brought themselves to attention. Dr. Nikola's dainty, languid manner +seemed to drop from him like a cloak, his eyes brightened, and his +voice, when he spoke, was clean cut as chiselled silver. + +"You are doubtless anxious to be informed why I summoned you from all +parts of the globe to meet me here to-night? And it is very natural you +should be. But then, from what you know of me, you should not be +surprised at anything I do." + +His voice dropped back into its old tone of gentle languor. He drew in a +great breath of smoke and then sent it slowly out from his lips again. +His eyes were half closed, and he drummed with one finger on the table +edge. The cat looked through the smoke at the three men, and it seemed +to them that he grew every moment larger and more ferocious. Presently +his owner took him from his perch, and seating him on his knee fell to +stroking his fur, from head to tail, with his long slim fingers. It was +as if he were drawing inspiration for some deadly mischief from the +uncanny beast. + +"To preface what I have to say to you, let me tell you that this is by +far the most important business for which I have ever required your +help. (Three slow strokes down the centre of the back, and one round +each ear.) When it first came into my mind I was at a loss who to trust +in the matter. I thought of Vendon, but I found Vendon was dead. I +thought of Brownlow, but Brownlow was no longer faithful. (Two strokes +down the back and two on the throat.) Then bit by bit I remembered you. +I was in Brazil at the time. So I sent for you. You came. So far so +good." + +He rose, and crossed over to the fireplace. As he went the cat crawled +back to its original position on his shoulder. Then his voice changed +once more to its former business-like tone. + +"I am not going to tell you very much about it. But from what I do tell +you, you will be able to gather a great deal and imagine the rest. To +begin with, there is a man living in this world to-day who has done me a +great and lasting injury. What that injury is is no concern of yours. +You would not understand if I told you. So we'll leave that out of the +question. He is immensely rich. His cheque for £300,000 would be +honoured by his bank at any minute. Obviously he is a power. He has had +reason to know that I am pitting my wits against his, and he flatters +himself that so far he has got the better of me. That is because I am +drawing him on. I am maturing a plan which will make him a poor and a +very miserable man at one and the same time. If that scheme succeeds, +and I am satisfied with the way you three men have performed the parts I +shall call on you to play in it, I shall pay to each of you the sum of +£10,000. If it doesn't succeed, then you will each receive a thousand +and your expenses. Do you follow me?" + +It was evident from their faces that they hung upon his every word. + +"But, remember, I demand from you your whole and entire labour. While +you are serving me you are mine body and soul. I know you are +trustworthy. I have had good proof that you are--pardon the +expression--unscrupulous, and I flatter myself you are silent. What is +more, I shall tell you nothing beyond what is necessary for the carrying +out of my scheme, so that you could not betray me if you would. Now for +my plans!" + +He sat down again and took a paper from his pocket. Having perused it, +he turned to Eastover. + +"You will leave at once--that is to say, by the boat on Wednesday--for +Sydney. You will book your passage to-morrow morning, first thing, and +join her in Plymouth. You will meet me to-morrow evening at an address I +will send you, and receive your final instructions. Good-night." + +Seeing that he was expected to go, Eastover rose, shook hands, and left +the room without a word. He was too astonished to hesitate or to say +anything. + +Nikola took another letter from his pocket and turned to Prendergast. +"*You* will go down to Dover to-night, cross to Paris to-morrow morning, +and leave this letter personally at the address you will find written on +it. On Thursday, at half-past two precisely, you will deliver me an +answer in the porch at Charing Cross. You will find sufficient money in +that envelope to pay all your expenses. Now go!" + +"At half-past two you shall have your answer. Good-night." + +"Good-night." + +When Prendergast had left the room, Dr. Nikola lit another cigar and +turned his attentions to Mr. Baxter. + +"Six months ago, Mr. Baxter, I found for you a situation as tutor to the +young Marquis of Beckenham. You still hold it, I suppose?" + +"I do." + +"Is the father well disposed towards you?" + +"In every way. I have done my best to ingratiate myself with him. That +was one of your instructions." + +"Yes, yes! But I was not certain that you would succeed. If the old man +is anything like what he was when I last met him he must still be a +difficult person to deal with. Does the boy like you?" + +"I hope so." + +"Have you brought me his photograph as I directed?" + +"I have. Here it is." + +Baxter took a photograph from his pocket and handed it across the table. + +"Good. You have done very well, Mr. Baxter. I am pleased with you. +To-morrow morning you will go back to Yorkshire----" + +"I beg your pardon, Bournemouth. His Grace owns a house near +Bournemouth, which he occupies during the summer months." + +"Very well--then to-morrow morning you will go back to Bournemouth and +continue to ingratiate yourself with father and son. You will also begin +to implant in the boy's mind a desire for travel. Don't let him become +aware that his desire has its source in you--but do not fail to foster +it all you can. I will communicate with you further in a day or two. Now +go." + +Baxter in his turn left the room. The door closed. Dr. Nikola picked up +the photograph and studied it. + +"The likeness is unmistakable--or it ought to be. My friend, my very +dear friend, Wetherell, my toils are closing on you. My arrangements are +perfecting themselves admirably. Presently, when all is complete, I +shall press the lever, the machinery will be set in motion, and you will +find yourself being slowly but surely ground into powder. Then you will +hand over what I want, and be sorry you thought fit to baulk Dr. +Nikola!" + +He rang the bell and ordered his bill. This duty discharged, he placed +the cat back in its prison, shut the lid, descended with the basket to +the hall, and called a hansom. The porter inquired to what address he +should order the cabman to drive. Dr. Nikola did not reply for a moment, +then he said, as if he had been thinking something out: "The *Green +Sailor* public-house, East India Dock Road." + + +------------------------ + +You can read the rest of "A Bid For Fortune; Or, Dr. Nikola's Vendetta" at `Open Library <https://archive.org/stream/bidforfortunenov00bootiala#page/12/mode/2up>`__ diff --git a/nikola/data/samplesite/pages/extending.rst b/nikola/data/samplesite/pages/extending.rst new file mode 120000 index 0000000..aab25e2 --- /dev/null +++ b/nikola/data/samplesite/pages/extending.rst @@ -0,0 +1 @@ +../../../../docs/extending.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/extending.txt b/nikola/data/samplesite/pages/extending.txt deleted file mode 120000 index f545532..0000000 --- a/nikola/data/samplesite/pages/extending.txt +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/extending.txt
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/internals.rst b/nikola/data/samplesite/pages/internals.rst new file mode 120000 index 0000000..23b276d --- /dev/null +++ b/nikola/data/samplesite/pages/internals.rst @@ -0,0 +1 @@ +../../../../docs/internals.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/internals.txt b/nikola/data/samplesite/pages/internals.txt deleted file mode 120000 index b955b57..0000000 --- a/nikola/data/samplesite/pages/internals.txt +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/internals.txt
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/manual.rst b/nikola/data/samplesite/pages/manual.rst index 9992900..4d5f0a1 120000 --- a/nikola/data/samplesite/pages/manual.rst +++ b/nikola/data/samplesite/pages/manual.rst @@ -1 +1 @@ -../../../../docs/manual.txt
\ No newline at end of file +../../../../docs/manual.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/path_handlers.rst b/nikola/data/samplesite/pages/path_handlers.rst new file mode 120000 index 0000000..23193d7 --- /dev/null +++ b/nikola/data/samplesite/pages/path_handlers.rst @@ -0,0 +1 @@ +../../../../docs/path_handlers.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/path_handlers.txt b/nikola/data/samplesite/pages/path_handlers.txt deleted file mode 120000 index cce056b..0000000 --- a/nikola/data/samplesite/pages/path_handlers.txt +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/path_handlers.txt
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/quickref.rst b/nikola/data/samplesite/pages/quickref.rst index 7cc91bd..152fbd0 100644 --- a/nikola/data/samplesite/pages/quickref.rst +++ b/nikola/data/samplesite/pages/quickref.rst @@ -8,7 +8,7 @@ .. raw:: html - <div class="alert alert-info pull-right" style="margin-left: 2em;"> + <div class="alert alert-primary float-md-right" style="margin-left: 2em;"> <h2><a name="contents">Contents</a></h2> <ul> @@ -1214,11 +1214,11 @@ <td> <samp>Titles are targets, too</samp> <br><samp>=======================</samp> - <br><samp>Implict references, like `Titles are</samp> + <br><samp>Implicit references, like `Titles are</samp> <br><samp>targets, too`_.</samp> <td> <font size="+2"><strong><a name="title">Titles are targets, too</a></strong></font> - <p>Implict references, like <a href="#title">Titles are + <p>Implicit references, like <a href="#title">Titles are targets, too</a>. </table> diff --git a/nikola/data/samplesite/pages/quickstart.rst b/nikola/data/samplesite/pages/quickstart.rst index 5937e56..28a452d 100644 --- a/nikola/data/samplesite/pages/quickstart.rst +++ b/nikola/data/samplesite/pages/quickstart.rst @@ -1,12 +1,9 @@ .. title: A reStructuredText Primer .. slug: quickstart .. date: 2012-03-30 23:00:00 UTC-03:00 -.. tags: -.. link: -.. description: - -A ReStructuredText Primer -========================= +.. tags: +.. link: +.. description: :Author: Richard Jones :Version: $Revision: 5801 $ @@ -23,7 +20,7 @@ reference. If these links don't work, please refer to the `master quick reference`_ document. __ -.. _Quick reStructuredText: quickref.html +.. _Quick reStructuredText: ../quickref/ .. _master quick reference: http://docutils.sourceforge.net/docs/user/rst/quickref.html @@ -65,7 +62,7 @@ Results in: This is another one. -__ quickref.html#paragraphs +__ ../quickref/#paragraphs Text styles @@ -73,7 +70,7 @@ Text styles (quickref__) -__ quickref.html#inline-markup +__ ../quickref/#inline-markup Inside paragraphs and other bodies of text, you may additionally mark text for *italics* with "``*italics*``" or **bold** with @@ -95,7 +92,7 @@ by enclosing it in double back-quotes (inline literals), like this:: ``*`` -__ quickref.html#escaping +__ ../quickref/#escaping .. Tip:: Think of inline markup as a form of (parentheses) and use it the same way: immediately before and after the text being marked @@ -119,7 +116,7 @@ Lists must always start a new paragraph -- that is, they must appear after a blank line. **enumerated** lists (numbers, letters or roman numerals; quickref__) - __ quickref.html#enumerated-lists + __ ../quickref/#enumerated-lists Start a line off with a number or letter followed by a period ".", right bracket ")" or surrounded by brackets "( )" -- whatever you're @@ -170,7 +167,7 @@ after a blank line. 1) and again **bulleted** lists (quickref__) - __ quickref.html#bullet-lists + __ ../quickref/#bullet-lists Just like enumerated lists, start the line off with a bullet point character - either "-", "+" or "*":: @@ -194,7 +191,7 @@ after a blank line. - another item **definition** lists (quickref__) - __ quickref.html#definition-lists + __ ../quickref/#definition-lists Unlike the other two, the definition lists consist of a term, and the definition of that term. The format of a definition list is:: @@ -222,7 +219,7 @@ Preformatting (code samples) ---------------------------- (quickref__) -__ quickref.html#literal-blocks +__ ../quickref/#literal-blocks To just include a chunk of preformatted, never-to-be-fiddled-with text, finish the prior paragraph with "``::``". The preformatted @@ -270,7 +267,7 @@ Sections (quickref__) -__ quickref.html#section-structure +__ ../quickref/#section-structure To break longer text up into sections, you use **section headers**. These are a single line of text (one or more words) with adornment: an @@ -364,9 +361,9 @@ Images (quickref__) -__ quickref.html#directives +__ ../quickref/#directives -To include an image in your document, you use the the ``image`` directive__. +To include an image in your document, you use the ``image`` directive__. For example:: .. image:: /images/nikola.png diff --git a/nikola/data/samplesite/pages/slides-demo.rst b/nikola/data/samplesite/pages/slides-demo.rst deleted file mode 100644 index 0d07bbc..0000000 --- a/nikola/data/samplesite/pages/slides-demo.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. title: Slides Demo -.. slug: slides-demo -.. date: 2012-12-27 10:16:20 UTC-03:00 -.. tags: -.. link: -.. description: - -Nikola intends to let you do slideshows easily: - -.. slides:: - - /galleries/demo/tesla_conducts_lg.jpg - /galleries/demo/tesla_lightning2_lg.jpg - /galleries/demo/tesla4_lg.jpg - /galleries/demo/tesla_lightning1_lg.jpg - /galleries/demo/tesla_tower1_lg.jpg - diff --git a/nikola/data/samplesite/pages/social_buttons.rst b/nikola/data/samplesite/pages/social_buttons.rst new file mode 120000 index 0000000..df8d07c --- /dev/null +++ b/nikola/data/samplesite/pages/social_buttons.rst @@ -0,0 +1 @@ +../../../../docs/social_buttons.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/social_buttons.txt b/nikola/data/samplesite/pages/social_buttons.txt deleted file mode 120000 index b60d598..0000000 --- a/nikola/data/samplesite/pages/social_buttons.txt +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/social_buttons.txt
\ No newline at end of file diff --git a/nikola/data/samplesite/pages/theming.rst b/nikola/data/samplesite/pages/theming.rst index d2dddb6..d004aa8 120000 --- a/nikola/data/samplesite/pages/theming.rst +++ b/nikola/data/samplesite/pages/theming.rst @@ -1 +1 @@ -../../../../docs/theming.txt
\ No newline at end of file +../../../../docs/theming.rst
\ No newline at end of file diff --git a/nikola/data/samplesite/posts/1.rst b/nikola/data/samplesite/posts/1.rst index 386251b..628dfd7 100644 --- a/nikola/data/samplesite/posts/1.rst +++ b/nikola/data/samplesite/posts/1.rst @@ -21,7 +21,6 @@ Next steps: * `Visit the Nikola website to learn more <https://getnikola.com>`__ * `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 a longer text <dr-nikolas-vendetta>` Send feedback to info@getnikola.com! diff --git a/nikola/data/symlinked.txt b/nikola/data/symlinked.txt index c0d37eb..477335e 100644 --- a/nikola/data/symlinked.txt +++ b/nikola/data/symlinked.txt @@ -1,142 +1,37 @@ -docs/sphinx/creating-a-site.txt -docs/sphinx/creating-a-theme.txt -docs/sphinx/extending.txt -docs/sphinx/internals.txt -docs/sphinx/manual.txt -docs/sphinx/path_handlers.txt -docs/sphinx/social_buttons.txt -docs/sphinx/theming.txt +docs/sphinx/creating-a-site.rst +docs/sphinx/creating-a-theme.rst +docs/sphinx/extending.rst +docs/sphinx/internals.rst +docs/sphinx/manual.rst +docs/sphinx/path_handlers.rst +docs/sphinx/social_buttons.rst +docs/sphinx/support.rst +docs/sphinx/template-variables.rst +docs/sphinx/theming.rst nikola/data/samplesite/pages/creating-a-theme.rst -nikola/data/samplesite/pages/extending.txt -nikola/data/samplesite/pages/internals.txt +nikola/data/samplesite/pages/extending.rst +nikola/data/samplesite/pages/internals.rst nikola/data/samplesite/pages/manual.rst -nikola/data/samplesite/pages/path_handlers.txt -nikola/data/samplesite/pages/social_buttons.txt +nikola/data/samplesite/pages/path_handlers.rst +nikola/data/samplesite/pages/social_buttons.rst nikola/data/samplesite/pages/theming.rst nikola/data/symlink-test-link.txt -nikola/data/themes/base/assets/js/moment-with-locales.min.js +nikola/data/themes/base/assets/css/baguetteBox.min.css +nikola/data/themes/base/assets/js/baguetteBox.min.js +nikola/data/themes/base/assets/js/html5.js +nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js +nikola/data/themes/base/assets/js/justified-layout.min.js +nikola/data/themes/base/assets/js/luxon.min.js nikola/data/themes/base/messages/messages_cz.py -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-theme.min.css.map -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/bootstrap.min.css.map -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 -nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff -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 -nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css -nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map -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/bootstrap.min.css.map -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 -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/data/themes/bootblog4-jinja/assets/css/bootblog.css +nikola/data/themes/bootblog4-jinja/bundles +nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css +nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js +nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js +nikola/data/themes/bootstrap4-jinja/bundles +nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css +nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js +nikola/data/themes/bootstrap4/assets/js/jquery.min.js +nikola/data/themes/bootstrap4/assets/js/popper.min.js nikola/plugins/command/auto/livereload.js diff --git a/nikola/data/themes/base-jinja/AUTHORS.txt b/nikola/data/themes/base-jinja/AUTHORS.txt deleted file mode 100644 index 043d497..0000000 --- a/nikola/data/themes/base-jinja/AUTHORS.txt +++ /dev/null @@ -1 +0,0 @@ -Roberto Alsina <https://github.com/ralsina> diff --git a/nikola/data/themes/base-jinja/base-jinja.theme b/nikola/data/themes/base-jinja/base-jinja.theme new file mode 100644 index 0000000..64dc002 --- /dev/null +++ b/nikola/data/themes/base-jinja/base-jinja.theme @@ -0,0 +1,10 @@ +[Theme] +engine = jinja +parent = base +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT + +[Family] +family = base +mako_version = base diff --git a/nikola/data/themes/base-jinja/engine b/nikola/data/themes/base-jinja/engine deleted file mode 100644 index 6f04b30..0000000 --- a/nikola/data/themes/base-jinja/engine +++ /dev/null @@ -1 +0,0 @@ -jinja diff --git a/nikola/data/themes/base-jinja/parent b/nikola/data/themes/base-jinja/parent deleted file mode 100644 index df967b9..0000000 --- a/nikola/data/themes/base-jinja/parent +++ /dev/null @@ -1 +0,0 @@ -base diff --git a/nikola/data/themes/base-jinja/templates/archive.tmpl b/nikola/data/themes/base-jinja/templates/archive.tmpl new file mode 100644 index 0000000..f2b715d --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/archive.tmpl @@ -0,0 +1 @@ +{% extends 'list_post.tmpl' %} diff --git a/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl b/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl new file mode 100644 index 0000000..ce913a3 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl @@ -0,0 +1,27 @@ +{# -*- coding: utf-8 -*- #} + +{% macro archive_navigation() %} +{% if 'archive_page' in pagekind %} + {% if has_archive_navigation %} + <nav class="archivenav"> + <ul class="pager"> + {% if previous_archive %} + <li class="previous"><a href="{{ previous_archive }}" rel="prev">{{ messages("Previous") }}</a></li> + {% else %} + <li class="previous disabled"><a href="#" rel="prev">{{ messages("Previous") }}</a></li> + {% endif %} + {% if up_archive %} + <li class="up"><a href="{{ up_archive }}" rel="up">{{ messages("Up") }}</a></li> + {% else %} + <li class="up disabled"><a href="#" rel="up">{{ messages("Up") }}</a></li> + {% endif %} + {% if next_archive %} + <li class="next"><a href="{{ next_archive }}" rel="next">{{ messages("Next") }}</a></li> + {% else %} + <li class="next disabled"><a href="#" rel="next">{{ messages("Next") }}</a></li> + {% endif %} + </ul> + </nav> + {% endif %} +{% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl index 8b9286e..a8bc9c6 100644 --- a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl +++ b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl @@ -1,13 +1,20 @@ {# -*- coding: utf-8 -*- #} {% extends 'index.tmpl' %} +{% import 'archive_navigation_helper.tmpl' as archive_nav with context %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% block extra_head %} {{ super() }} - {% if translations|length > 1 and generate_atom %} - {% 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 %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ archive_name }} archive" href="{{ _link("archive_atom", archive_name) }}"> - {% endif %} + {{ feeds_translations.head(archive_name, kind, rss_override=False) }} +{% endblock %} + +{% block content_header %} + <header> + <h1>{{ title|e }}</h1> + {{ archive_nav.archive_navigation() }} + <div class="metadata"> + {{ feeds_translations.feed_link(archive, kind) }} + {{ feeds_translations.translation_link(kind) }} + </div> + </header> {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/author.tmpl b/nikola/data/themes/base-jinja/templates/author.tmpl index 327debe..4d8a876 100644 --- a/nikola/data/themes/base-jinja/templates/author.tmpl +++ b/nikola/data/themes/base-jinja/templates/author.tmpl @@ -1,43 +1,28 @@ {# -*- coding: utf-8 -*- #} {% extends 'list_post.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% block extra_head %} - {{ super() }} - {% if translations|length > 1 and generate_rss %} - {% for language in translations|sort %} - <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ author|e }} ({{ language }})" href="{{ _link(kind + "_rss", author, language) }}"> - {% endfor %} - {% elif generate_rss %} - <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ author|e }}" href="{{ _link(kind + "_rss", author) }}"> - {% endif %} + {{ feeds_translations.head(author, kind, rss_override=False) }} {% endblock %} - {% block content %} <article class="authorpage"> <header> <h1>{{ title|e }}</h1> {% if description %} - <p>{{ description }}</p> + <p>{{ description }}</p> {% endif %} <div class="metadata"> - {% if translations|length > 1 and generate_rss %} - {% for language in translations|sort %} - <p class="feedlink"> - <a href="{{ _link(kind + "_rss", author, language) }}" hreflang="{{ language }}" type="application/rss+xml">{{ messages('RSS feed', language) }} ({{ language }})</a> - </p> - {% endfor %} - {% elif generate_rss %} - <p class="feedlink"><a href="{{ _link(kind + "_rss", author) }}" type="application/rss+xml">{{ messages('RSS feed') }}</a></p> - {% endif %} + {{ feeds_translations.feed_link(author, kind) }} </div> </header> {% if posts %} - <ul class="postlist"> - {% for post in posts %} - <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a></li> - {% endfor %} - </ul> + <ul class="postlist"> + {% for post in posts %} + <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a></li> + {% endfor %} + </ul> {% endif %} </article> {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/authorindex.tmpl b/nikola/data/themes/base-jinja/templates/authorindex.tmpl index 3c40ee1..4b2fcbf 100644 --- a/nikola/data/themes/base-jinja/templates/authorindex.tmpl +++ b/nikola/data/themes/base-jinja/templates/authorindex.tmpl @@ -1,13 +1,21 @@ {# -*- coding: utf-8 -*- #} {% extends 'index.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block content_header %} + <header> + <h1>{{ title|e }}</h1> + {% if description %} + <p>{{ description }}</p> + {% endif %} + <div class="metadata"> + {{ feeds_translations.feed_link(author, kind) }} + {{ feeds_translations.translation_link(kind) }} + </div> + </header> +{% endblock %} {% block extra_head %} {{ super() }} - {% if tranlations|length > 1 and generate_atom %} - {% for language in translations|sort %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ author|e }} section ({{ language }})" href="{{ _link(kind + "_atom", author, language) }}"> - {% endfor %} - {% elif generate_atom %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ author|e }} section" href="{{ _link("author" + "_atom", author) }}"> - {% endif %} + {{ feeds_translations.head(author, kind, rss_override=False) }} {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/authors.tmpl b/nikola/data/themes/base-jinja/templates/authors.tmpl index 8b6ea64..c8e05ff 100644 --- a/nikola/data/themes/base-jinja/templates/authors.tmpl +++ b/nikola/data/themes/base-jinja/templates/authors.tmpl @@ -1,10 +1,18 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block extra_head %} + {{ feeds_translations.head(kind=kind, feeds=False) }} +{% endblock %} {% block content %} <article class="authorindex"> {% if items %} <h2>{{ messages("Authors") }}</h2> + <div class="metadata"> + {{ feeds_translations.translation_link(kind) }} + </div> <ul class="postlist"> {% for text, link in items %} {% if text not in hidden_authors %} diff --git a/nikola/data/themes/base-jinja/templates/base.tmpl b/nikola/data/themes/base-jinja/templates/base.tmpl index 5412326..8b057db 100644 --- a/nikola/data/themes/base-jinja/templates/base.tmpl +++ b/nikola/data/themes/base-jinja/templates/base.tmpl @@ -2,8 +2,8 @@ {% import 'base_helper.tmpl' as base with context %} {% import 'base_header.tmpl' as header with context %} {% import 'base_footer.tmpl' as footer with context %} -{% import 'annotation_helper.tmpl' as annotations with context %} {{ set_locale(lang) }} +{# <html> tag is included by base.html_headstart #} {{ base.html_headstart() }} {% block extra_head %} {# Leave this block alone. #} @@ -11,16 +11,29 @@ {{ template_hooks['extra_head']() }} </head> <body> -<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a> + <a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a> <div id="container"> - {{ header.html_header() }} - <main id="content"> + {{ header.html_header() }} + <main id="content"> {% block content %}{% endblock %} - </main> - {{ footer.html_footer() }} + </main> + {{ footer.html_footer() }} </div> {{ base.late_load_js() }} + {% if date_fanciness != 0 %} + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}"; + fancydates({{ date_fanciness }}, {{ luxon_date_format }}); + </script> + <!-- end fancy dates --> + {% endif %} {% block extra_js %}{% endblock %} + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> {{ body_end }} {{ template_hooks['body_end']() }} </body> diff --git a/nikola/data/themes/base-jinja/templates/base_footer.tmpl b/nikola/data/themes/base-jinja/templates/base_footer.tmpl index 2e541a6..db44a67 100644 --- a/nikola/data/themes/base-jinja/templates/base_footer.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_footer.tmpl @@ -1,5 +1,4 @@ {# -*- coding: utf-8 -*- #} -{% import 'base_helper.tmpl' as base with context %} {% macro html_footer() %} {% if content_footer %} diff --git a/nikola/data/themes/base-jinja/templates/base_header.tmpl b/nikola/data/themes/base-jinja/templates/base_header.tmpl index d9370d7..bfbd447 100644 --- a/nikola/data/themes/base-jinja/templates/base_header.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_header.tmpl @@ -16,7 +16,7 @@ {% endmacro %} {% macro html_site_title() %} - <h1 id="brand"><a href="{{ abs_link(_link("root", None, lang)) }}" title="{{ blog_title|e }}" rel="home"> + <h1 id="brand"><a href="{{ _link("root", None, lang) }}" title="{{ blog_title|e }}" rel="home"> {% if logo_url %} <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo"> {% endif %} @@ -30,13 +30,22 @@ {% macro html_navigation_links() %} <nav id="menu"> <ul> - {% for url, text in navigation_links[lang] %} + {{ html_navigation_links_entries(navigation_links) }} + {{ html_navigation_links_entries(navigation_alt_links) }} + {{ template_hooks['menu']() }} + {{ template_hooks['menu_alt']() }} + </ul> + </nav> +{% endmacro %} + +{% macro html_navigation_links_entries(navigation_links_source) %} + {% for url, text in navigation_links_source[lang] %} {% if isinstance(url, tuple) %} <li> {{ text }} <ul> {% for suburl, text in url %} {% if rel_link(permalink, suburl) == "#" %} - <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a></li> + <li class="active"><a href="{{ permalink }}">{{ text }}<span class="sr-only"> {{ messages("(active)", lang) }}</span></a></li> {% else %} <li><a href="{{ suburl }}">{{ text }}</a></li> {% endif %} @@ -44,16 +53,12 @@ </ul> {% else %} {% if rel_link(permalink, url) == "#" %} - <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a></li> + <li class="active"><a href="{{ permalink }}">{{ text }}<span class="sr-only"> {{ messages("(active)", lang) }}</span></a></li> {% else %} <li><a href="{{ url }}">{{ text }}</a></li> {% endif %} {% endif %} {% endfor %} - {{ template_hooks['menu']() }} - {{ template_hooks['menu_alt']() }} - </ul> - </nav> {% endmacro %} {% macro html_translation_header() %} diff --git a/nikola/data/themes/base-jinja/templates/base_helper.tmpl b/nikola/data/themes/base-jinja/templates/base_helper.tmpl index 04f49fe..a05abb9 100644 --- a/nikola/data/themes/base-jinja/templates/base_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/base_helper.tmpl @@ -1,31 +1,25 @@ {# -*- coding: utf-8 -*- #} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% macro html_headstart() %} <!DOCTYPE html> <html \ -prefix=' -{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %} -og: http://ogp.me/ns# article: http://ogp.me/ns/article# -{% endif %} -{% if comment_system == 'facebook' %} -fb: http://ogp.me/ns/fb# -{% endif %} -' \ -{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %} -vocab="http://ogp.me/ns" \ -{% endif %} + prefix=' + og: http://ogp.me/ns# article: http://ogp.me/ns/article# + {% if comment_system == 'facebook' %} + fb: http://ogp.me/ns/fb# + {% endif %} + ' \ + vocab="http://ogp.me/ns" \ {% if is_rtl %} -dir="rtl" + dir="rtl" {% endif %} lang="{{ lang }}"> <head> <meta charset="utf-8"> - {% if use_base_tag %} - <base href="{{ abs_link(permalink) }}"> - {% endif %} {% if description %} - <meta name="description" content="{{ description|e }}"> + <meta name="description" content="{{ description|e }}"> {% endif %} <meta name="viewport" content="width=device-width"> {% if title == blog_title %} @@ -35,8 +29,11 @@ lang="{{ lang }}"> {% endif %} {{ html_stylesheets() }} - <meta content="{{ theme_color }}" name="theme-color"> - {{ html_feedlinks() }} + <meta name="theme-color" content="{{ theme_color }}"> + {% if meta_generator_tag %} + <meta name="generator" content="Nikola (getnikola.com)"> + {% endif %} + {{ feeds_translations.head(classification=None, kind='index', other=False) }} <link rel="canonical" href="{{ abs_link(permalink) }}"> {% if favicons %} @@ -56,29 +53,58 @@ lang="{{ lang }}"> <link rel="next" href="{{ nextlink }}" type="text/html"> {% endif %} - {{ mathjax_config }} {% if use_cdn %} - <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + <!--[if lt IE 9]><script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script><![endif]--> {% else %} - <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang) }}"></script><![endif]--> + <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5shiv-printshiv.min.js', lang, url_type) }}"></script><![endif]--> {% endif %} {{ extra_head_data }} {% endmacro %} {% macro late_load_js() %} + {% if use_bundles %} + {% if use_cdn %} + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + <script src="/assets/js/all.js"></script> + {% else %} + <script src="/assets/js/all-nocdn.js"></script> + {% endif %} + {% else %} + {% if use_cdn %} + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + {% else %} + <script src="/assets/js/baguetteBox.min.js"></script> + {% endif %} + {% endif %} + {% if date_fanciness != 0 %} + {% if date_fanciness == 2 %} + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script> + {% endif %} + {% if use_cdn %} + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + {% else %} + <script src="/assets/js/luxon.min.js"></script> + {% endif %} + {% if not use_bundles %} + <script src="/assets/js/fancydates.min.js"></script> + {% endif %} + {% endif %} {{ social_buttons_code }} {% endmacro %} {% macro html_stylesheets() %} {% if use_bundles %} {% if use_cdn %} + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> {% else %} <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> {% endif %} {% else %} - <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst_base.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/nikola_rst.css" rel="stylesheet" type="text/css"> <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"> {% if has_custom_css %} @@ -91,34 +117,16 @@ lang="{{ lang }}"> {% endif %} {% endmacro %} +{# This function is deprecated; use feed_helper directly. #} {% macro html_feedlinks() %} - {% if rss_link %} - {{ rss_link }} - {% elif generate_rss %} - {% if translations|length > 1 %} - {% for language in translations|sort %} - <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|sort %} - <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 %} + {{ feeds_translations.head(classification=None, kind='index', other=False) }} {% endmacro %} {% macro html_translations() %} <ul class="translations"> {% 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> + <li><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li> {% endif %} {% endfor %} </ul> diff --git a/nikola/data/themes/base-jinja/templates/comments_helper.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl index aba7294..2a7d8dc 100644 --- a/nikola/data/themes/base-jinja/templates/comments_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl @@ -1,63 +1,63 @@ {# -*- coding: utf-8 -*- #} {% import 'comments_helper_disqus.tmpl' as disqus with context %} -{% import 'comments_helper_livefyre.tmpl' as livefyre with context %} {% import 'comments_helper_intensedebate.tmpl' as intensedebate with context %} {% import 'comments_helper_muut.tmpl' as muut with context %} -{% import 'comments_helper_googleplus.tmpl' as googleplus with context %} {% import 'comments_helper_facebook.tmpl' as facebook with context %} {% import 'comments_helper_isso.tmpl' as isso with context %} +{% import 'comments_helper_commento.tmpl' as commento with context %} +{% import 'comments_helper_utterances.tmpl' as utterances with context %} {% macro comment_form(url, title, identifier) %} {% if comment_system == 'disqus' %} {{ disqus.comment_form(url, title, identifier) }} - {% elif comment_system == 'livefyre' %} - {{ livefyre.comment_form(url, title, identifier) }} {% elif comment_system == 'intensedebate' %} {{ intensedebate.comment_form(url, title, identifier) }} {% elif comment_system == 'muut' %} {{ muut.comment_form(url, title, identifier) }} - {% elif comment_system == 'googleplus' %} - {{ googleplus.comment_form(url, title, identifier) }} {% elif comment_system == 'facebook' %} {{ facebook.comment_form(url, title, identifier) }} {% elif comment_system == 'isso' %} {{ isso.comment_form(url, title, identifier) }} + {% elif comment_system == 'commento' %} + {{ commento.comment_form(url, title, identifier) }} + {% elif comment_system == 'utterances' %} + {{ utterances.comment_form(url, title, identifier) }} {% endif %} {% endmacro %} {% macro comment_link(link, identifier) %} {% if comment_system == 'disqus' %} {{ disqus.comment_link(link, identifier) }} - {% elif comment_system == 'livefyre' %} - {{ livefyre.comment_link(link, identifier) }} {% elif comment_system == 'intensedebate' %} {{ intensedebate.comment_link(link, identifier) }} {% elif comment_system == 'muut' %} {{ muut.comment_link(link, identifier) }} - {% elif comment_system == 'googleplus' %} - {{ googleplus.comment_link(link, identifier) }} {% elif comment_system == 'facebook' %} {{ facebook.comment_link(link, identifier) }} {% elif comment_system == 'isso' %} {{ isso.comment_link(link, identifier) }} + {% elif comment_system == 'commento' %} + {{ commento.comment_link(link, identifier) }} + {% elif comment_system == 'utterances' %} + {{ utterances.comment_link(link, identifier) }} {% endif %} {% endmacro %} {% macro comment_link_script() %} {% if comment_system == 'disqus' %} {{ disqus.comment_link_script() }} - {% elif comment_system == 'livefyre' %} - {{ livefyre.comment_link_script() }} {% elif comment_system == 'intensedebate' %} {{ intensedebate.comment_link_script() }} {% elif comment_system == 'muut' %} {{ muut.comment_link_script() }} - {% elif comment_system == 'googleplus' %} - {{ googleplus.comment_link_script() }} {% elif comment_system == 'facebook' %} {{ facebook.comment_link_script() }} {% elif comment_system == 'isso' %} {{ isso.comment_link_script() }} + {% elif comment_system == 'commento' %} + {{ commento.comment_link_script() }} + {% elif comment_system == 'utterances' %} + {{ utterances.comment_link_script() }} {% endif %} {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl new file mode 100644 index 0000000..25857d9 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl @@ -0,0 +1,13 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} + <div id="commento"></div> + + <script defer src="{{ comment_system_id }}/js/commento.js"></script> +{% endmacro %} + +{% macro comment_link(link, identifier) %} + <a href="{{ link }}#commento">{{ messages("Comments") }}</a> +{% endmacro %} + +{% macro comment_link_script() %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl index 981453d..4aa42fa 100644 --- a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl +++ b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl @@ -30,7 +30,7 @@ {% macro comment_link(link, identifier) %} {% if comment_system_id %} - <a href="{{ link }}#disqus_thread" data-disqus-identifier="{{ identifier }}">Comments</a> + <a href="{{ link }}#disqus_thread" data-disqus-identifier="{{ identifier }}">{{ messages("Comments") }}</a> {% endif %} {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl deleted file mode 100644 index cf153e0..0000000 --- a/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{# -*- coding: utf-8 -*- #} -{% macro comment_form(url, title, identifier) %} -<script src="https://apis.google.com/js/plusone.js"></script> -<div class="g-comments" - data-href="{{ url }}" - data-first_party_property="BLOGGER" - data-view_type="FILTERED_POSTMOD"> -</div> -{% endmacro %} - -{% macro comment_link(link, identifier) %} -<div class="g-commentcount" data-href="{{ link }}"></div> -<script src="https://apis.google.com/js/plusone.js"></script> -{% endmacro %} - -{% macro comment_link_script() %} -{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl index 042409b..d649d31 100644 --- a/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl +++ b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl @@ -6,7 +6,7 @@ var idcomments_post_id = "{{ identifier }}"; var idcomments_post_url = "{{ url }}"; </script> <span id="IDCommentsPostTitle" style="display:none"></span> -<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script> +<script src="https://www.intensedebate.com/js/genericCommentWrapperV2.js"></script> </script> {% endmacro %} @@ -17,7 +17,7 @@ var idcomments_acct = '{{ comment_system_id }}'; var idcomments_post_id = "{{ identifier }}"; var idcomments_post_url = "{{ link }}"; </script> -<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> +<script src="https://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> </a> {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl index b40b5e4..43995c5 100644 --- a/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl +++ b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl @@ -1,20 +1,26 @@ {# -*- coding: utf-8 -*- #} {% macro comment_form(url, title, identifier) %} {% if comment_system_id %} - <div data-title="{{ title|urlencode }}" id="isso-thread"></div> - <script src="{{ comment_system_id }}js/embed.min.js" data-isso="{{ comment_system_id }}"></script> + <div data-title="{{ title|e }}" id="isso-thread"></div> + <script src="{{ comment_system_id }}js/embed.min.js" data-isso="{{ comment_system_id }}" data-isso-lang="{{ lang }}" + {% if isso_config %} + {% for k, v in isso_config.items() %} + data-isso-{{ k }}="{{ v }}" + {% endfor %} + {% endif %} + ></script> {% endif %} {% endmacro %} {% macro comment_link(link, identifier) %} {% if comment_system_id %} - <a href="{{ link }}#isso-thread">Comments</a> + <a href="{{ link }}#isso-thread">{{ messages("Comments") }}</a> {% endif %} {% endmacro %} {% macro comment_link_script() %} {% if comment_system_id and 'index' in pagekind %} - <script src="{{ comment_system_id }}js/count.min.js" data-isso="{{ comment_system_id }}"></script> + <script src="{{ comment_system_id }}js/count.min.js" data-isso="{{ comment_system_id }}" data-isso-lang="{{ lang }}"></script> {% endif %} {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl deleted file mode 100644 index 5b01fbf..0000000 --- a/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -{# -*- coding: utf-8 -*- #} -{% macro comment_form(url, title, identifier) %} -<div id="livefyre-comments"></div> -<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script> -<script> -(function () { - var articleId = "{{ identifier }}"; - fyre.conv.load({}, [{ - el: 'livefyre-comments', - network: "livefyre.com", - siteId: "{{ comment_system_id }}", - articleId: articleId, - signed: false, - collectionMeta: { - articleId: articleId, - url: fyre.conv.load.makeCollectionUrl(), - } - }], function() {}); -}()); -</script> -{% endmacro %} - -{% macro comment_link(link, identifier) %} - <a href="{{ link }}"> - <span class="livefyre-commentcount" data-lf-site-id="{{ comment_system_id }}" data-lf-article-id="{{ identifier }}"> - 0 Comments - </span> -{% endmacro %} - - -{% macro comment_link_script() %} -<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script> -{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl deleted file mode 100644 index 8912e19..0000000 --- a/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -{# -*- coding: utf-8 -*- #} -{% import 'comments_helper.tmpl' as comments with context %} -{% if not post.meta('nocomments') %} - {{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }} -{% endif %} diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl new file mode 100644 index 0000000..e1c03c2 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl @@ -0,0 +1,23 @@ +{# -*- coding: utf-8 -*- #} +{% macro comment_form(url, title, identifier) %} + {% if comment_system_id %} + <div data-title="{{ title|e }}" id="utterances-thread"></div> + <script src="https://utteranc.es/client.js" repo="{{ comment_system_id }}" + {% if utterances_config %} + {% for k, v in utterances_config.items() %} + {{ k }}="{{ v }}" + {% endfor %} + {% endif %} + ></script> + {% endif %} +{% endmacro %} + +{% macro comment_link(link, identifier) %} + {% if comment_system_id %} + <a href="{{ link }}#utterances-thread">{{ messages("Comments") }}</a> + {% endif %} +{% endmacro %} + + +{% macro comment_link_script() %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl b/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl new file mode 100644 index 0000000..278e1c4 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl @@ -0,0 +1,124 @@ +{# -*- coding: utf-8 -*- #} + +{% macro _head_feed_link(link_type, link_name, link_postfix, classification, kind, language) %} + {% if translations|length > 1 %} + <link rel="alternate" type="{{ link_type }}" title="{{ link_name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + '_' + link_postfix, classification, language) }}"> + {% else %} + <link rel="alternate" type="{{ link_type }}" title="{{ link_name|e }}" hreflang="{{ language }}" href="{{ _link(kind + '_' + link_postfix, classification, language) }}"> + {% endif %} +{% endmacro %} + +{% macro _html_feed_link(link_type, link_name, link_postfix, classification, kind, language, name=None) %} + {% if translations|length > 1 %} + {% if name and kind != "archive" and kind != "author" %} + <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ name|e }}, {{ language }})</a> + {% else %} + <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ language }})</a> + {% endif %} + {% else %} + {% if name and kind != "archive" and kind != "author" %} + <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ name|e }})</a> + {% else %} + <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }}</a> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro _html_translation_link(classification, kind, language, name=None) %} + {% if name and kind != "archive" and kind != "author" %} + <a href="{{ _link(kind, classification, language) }}" hreflang="{{ language }}" rel="alternate">{{ messages("LANGUAGE", language) }} ({{ name|e }})</a> + {% else %} + <a href="{{ _link(kind, classification, language) }}" hreflang="{{ language }}" rel="alternate">{{ messages("LANGUAGE", language) }}</a> + {% endif %} +{% endmacro %} + +{% macro _head_rss(classification=None, kind='index', rss_override=True) %} + {% if rss_link and rss_override %} + {{ rss_link }} + {% endif %} + {% if generate_rss and not (rss_link and rss_override) and kind != 'archive' %} + {% if translations|length > 1 and has_other_languages and classification and kind != 'index' %} + {% for language, classification, name in all_languages %} + <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + "_rss", classification, language) }}"> + {% endfor %} + {% else %} + {% for language in translations_feedorder %} + {% if (classification or classification == '') and kind != 'index' %} + {{ _head_feed_link('application/rss+xml', 'RSS for ' + kind + ' ' + classification, 'rss', classification, kind, language) }} + {% else %} + {{ _head_feed_link('application/rss+xml', 'RSS', 'rss', classification, 'index', language) }} + {% endif %} + {% endfor %} + {% endif %} + {% endif %} +{% endmacro %} + +{% macro _head_atom(classification=None, kind='index') %} + {% if generate_atom %} + {% if translations|length > 1 and has_other_languages and classification and kind != 'index' %} + {% for language, classification, name in all_languages %} + <link rel="alternate" type="application/atom+xml" title="Atom for {{ kind }} {{ name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + "_atom", classification, language) }}"> + {% endfor %} + {% else %} + {% for language in translations_feedorder %} + {% if (classification or classification == '') and kind != 'index' %} + {{ _head_feed_link('application/atom+xml', 'Atom for ' + kind + ' ' + classification, 'atom', classification, kind, language) }} + {% else %} + {{ _head_feed_link('application/atom+xml', 'Atom', 'atom', classification, 'index', language) }} + {% endif %} + {% endfor %} + {% endif %} + {% endif %} +{% endmacro %} + +{# Handles both feeds and translations #} +{% macro head(classification=None, kind='index', feeds=True, other=True, rss_override=True, has_no_feeds=False) %} + {% if feeds and not has_no_feeds %} + {{ _head_rss(classification, 'index' if (kind == 'archive' and rss_override) else kind, rss_override) }} + {{ _head_atom(classification, kind) }} + {% endif %} + {% if other and has_other_languages and other_languages %} + {% for language, classification, _ in other_languages %} + <link rel="alternate" hreflang="{{ language }}" href="{{ _link(kind, classification, language) }}"> + {% endfor %} + {% endif %} +{% endmacro %} + +{% macro feed_link(classification, kind) %} + {% if generate_atom or generate_rss %} + {% if translations|length > 1 and has_other_languages and kind != 'index' %} + {% for language, classification, name in all_languages %} + <p class="feedlink"> + {% if generate_atom %} + {{ _html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language, name) }} + {% endif %} + {% if generate_rss and kind != 'archive' %} + {{ _html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language, name) }} + {% endif %} + </p> + {% endfor %} + {% else %} + {% for language in translations_feedorder %} + <p class="feedlink"> + {% if generate_atom %} + {{ _html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language) }} + {% endif %} + {% if generate_rss and kind != 'archive' %} + {{ _html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language) }} + {% endif %} + </p> + {% endfor %} + {% endif %} + {% endif %} +{% endmacro %} + +{% macro translation_link(kind) %} + {% if has_other_languages and other_languages %} + <div class="translationslist translations"> + <h3 class="translationslist-intro">{{ messages("Also available in:") }}</h3> + {% for language, classification, name in other_languages %} + <p>{{ _html_translation_link(classification, kind, language, name) }}</p> + {% endfor %} + </div> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/gallery.tmpl b/nikola/data/themes/base-jinja/templates/gallery.tmpl index 977dea1..d425106 100644 --- a/nikola/data/themes/base-jinja/templates/gallery.tmpl +++ b/nikola/data/themes/base-jinja/templates/gallery.tmpl @@ -1,11 +1,12 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} {% import 'comments_helper.tmpl' as comments with context %} -{% import 'crumbs.tmpl' as ui with context %} +{% import 'ui_helper.tmpl' as ui with context %} +{% import 'post_helper.tmpl' as post_helper with context %} {% block sourcelink %}{% endblock %} {% block content %} - {{ ui.bar(crumbs) }} + {{ ui.breadcrumbs(crumbs) }} {% if title %} <h1>{{ title|e }}</h1> {% endif %} @@ -15,21 +16,39 @@ </p> {% endif %} {% if folders %} - <ul> - {% for folder, ftitle in folders %} - <li><a href="{{ folder }}"><i - class="icon-folder-open"></i> {{ ftitle|e }}</a></li> - {% endfor %} - </ul> - {% endif %} - {% if photo_array %} - <ul class="thumbnails"> - {% for image in photo_array %} - <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title'] }}"> - <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" /></a> - {% endfor %} - </ul> + {% if galleries_use_thumbnail %} + {% for (folder, ftitle, fpost) in folders %} + <div class="thumnbnail-container"> + <a href="{{ folder }}" class="thumbnail image-reference" title="{{ ftitle|e }}"> + {% if fpost and fpost.previewimage %} + <img src="{{ fpost.previewimage }}" alt="{{ ftitle|e }}" loading="lazy" style="max-width:{{ thumbnail_size }}px; max-height:{{ thumbnail_size }}px;" /> + {% else %} + <div style="height: {{ thumbnail_size }}px; width: {{ thumbnail_size }}px; background-color: #eee;"></div> + {% endif %} + <p class="thumbnail-caption">{{ ftitle|e }}</p> + </a> + </div> + {% endfor %} + {% else %} + <ul> + {% for folder, ftitle in folders %} + <li><a href="{{ folder }}">📂 {{ ftitle|e }}</a></li> + {% endfor %} + </ul> + {% endif %} {% 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']|e }}"> + <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" loading="lazy" /></a> + {% endfor %} +</ul> +</noscript> +{% endif %} {% if site_has_comments and enable_comments %} {{ comments.comment_form(None, permalink, title) }} {% endif %} @@ -38,4 +57,35 @@ {% block extra_head %} {{ super() }} <link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> +<style type="text/css"> + #gallery_container { + position: relative; + } + .image-block { + position: absolute; + } +</style> +{% if translations|length > 1 %} + {% for langname in translations.keys() %} + {% if langname != lang %} + <link rel="alternate" hreflang="{{ langname }}" href="{{ _link('gallery', gallery_path, langname) }}"> + {% endif %} + {% endfor %} +{% endif %} +<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> +{% if post %} + {{ post_helper.open_graph_metadata(post) }} + {{ post_helper.twitter_card_information(post) }} +{% endif %} +{% endblock %} + +{% block extra_js %} +<script src="/assets/js/justified-layout.min.js"></script> +<script src="/assets/js/gallery.min.js"></script> +<script> +var jsonContent = {{ photo_array_json }}; +var thumbnailSize = {{ thumbnail_size }}; +renderGallery(jsonContent, thumbnailSize); +window.addEventListener('resize', function(){renderGallery(jsonContent, thumbnailSize)}); +</script> {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/index.tmpl b/nikola/data/themes/base-jinja/templates/index.tmpl index f982091..55ae9aa 100644 --- a/nikola/data/themes/base-jinja/templates/index.tmpl +++ b/nikola/data/themes/base-jinja/templates/index.tmpl @@ -1,6 +1,9 @@ {# -*- coding: utf-8 -*- #} {% import 'index_helper.tmpl' as helper with context %} +{% import 'math_helper.tmpl' as math with context %} {% import 'comments_helper.tmpl' as comments with context %} +{% import 'pagination_helper.tmpl' as pagination with context %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% extends 'base.tmpl' %} {% block extra_head %} @@ -8,27 +11,45 @@ {% if posts and (permalink == '/' or permalink == '/' + index_file) %} <link rel="prefetch" href="{{ posts[0].permalink() }}" type="text/html"> {% endif %} + {{ math.math_styles_ifposts(posts) }} {% endblock %} {% block content %} -{% block content_header %}{% endblock %} +{% block content_header %} + {{ feeds_translations.translation_link(kind) }} +{% endblock %} {% if 'main_index' in pagekind %} {{ front_index_header }} {% endif %} +{% if page_links %} + {{ pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed) }} +{% endif %} <div class="postindex"> {% for post in posts %} - <article class="h-entry post-{{ post.meta('type') }}"> + <article class="h-entry post-{{ post.meta('type') }}" itemscope="itemscope" itemtype="http://schema.org/Article"> <header> <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"> - {% if author_pages_generated %} + <p class="byline author vcard"><span class="byline-name fn" itemprop="author"> + {% if author_pages_generated and multiple_authors_per_post %} + {% for author in post.authors() %} + <a href="{{ _link('author', author) }}">{{ author|e }}</a> + {% endfor %} + {% elif author_pages_generated %} <a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a> {% else %} {{ post.author()|e }} {% endif %} </span></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)|e }}">{{ post.formatted_date(date_format)|e }}</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)|e }}">{{ post.formatted_date(date_format)|e }}</time> + {% if post.updated and post.updated != post.date %} + <span class="updated"> ({{ messages("updated") }} + <time class="dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span> + {% endif %} + </a> + </p> {% if not post.meta('nocomments') and site_has_comments %} <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} {% endif %} @@ -47,5 +68,5 @@ </div> {{ helper.html_pager() }} {{ comments.comment_link_script() }} -{{ helper.mathjax_script(posts) }} +{{ math.math_scripts_ifposts(posts) }} {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/index_helper.tmpl b/nikola/data/themes/base-jinja/templates/index_helper.tmpl index 704c635..bc57734 100644 --- a/nikola/data/themes/base-jinja/templates/index_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/index_helper.tmpl @@ -1,4 +1,5 @@ {# -*- coding: utf-8 -*- #} +{% import 'math_helper.tmpl' as math with context %} {% macro html_pager() %} {% if prevlink or nextlink %} <nav class="postindexpager"> @@ -18,33 +19,7 @@ {% endif %} {% endmacro %} +{# This function is deprecated; use math_helper directly. #} {% macro mathjax_script(posts) %} - {% if posts|selectattr("is_mathjax")|list %} - {% if use_katex %} - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script> - {% if katex_auto_render %} - <script> - renderMathInElement(document.body, - { - {{ katex_auto_render }} - } - ); - </script> - {% else %} - <script> - renderMathInElement(document.body); - </script> - {% endif %} - {% else %} - <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script> - {% if mathjax_config %} - {{ mathjax_config }} - {% else %} - <script type="text/x-mathjax-config"> - MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); - </script> - {% endif %} - {% endif %} - {% endif %} + {{ math.math_scripts_ifposts(posts) }} {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/list.tmpl b/nikola/data/themes/base-jinja/templates/list.tmpl index c9c330b..5f8ddea 100644 --- a/nikola/data/themes/base-jinja/templates/list.tmpl +++ b/nikola/data/themes/base-jinja/templates/list.tmpl @@ -1,11 +1,19 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} +{% import 'archive_navigation_helper.tmpl' as archive_nav with context %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block extra_head %} + {{ feeds_translations.head(kind=kind, rss_override=False, has_no_feeds=has_no_feeds) }} +{% endblock %} {% block content %} <article class="listpage"> <header> <h1>{{ title|e }}</h1> </header> + {{ archive_nav.archive_navigation() }} + {{ feeds_translations.translation_link(kind) }} {% if items %} <ul class="postlist"> {% for text, link, count in items %} diff --git a/nikola/data/themes/base-jinja/templates/list_post.tmpl b/nikola/data/themes/base-jinja/templates/list_post.tmpl index 1dd2605..e6b2080 100644 --- a/nikola/data/themes/base-jinja/templates/list_post.tmpl +++ b/nikola/data/themes/base-jinja/templates/list_post.tmpl @@ -1,11 +1,19 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} +{% import 'archive_navigation_helper.tmpl' as archive_nav with context %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block extra_head %} + {{ feeds_translations.head(kind=kind, rss_override=False) }} +{% endblock %} {% block content %} <article class="listpage"> <header> <h1>{{ title|e }}</h1> </header> + {{ archive_nav.archive_navigation() }} + {{ feeds_translations.translation_link(kind) }} {% if posts %} <ul class="postlist"> {% for post in posts %} diff --git a/nikola/data/themes/base-jinja/templates/listing.tmpl b/nikola/data/themes/base-jinja/templates/listing.tmpl index 9b6d76d..7b6b3a6 100644 --- a/nikola/data/themes/base-jinja/templates/listing.tmpl +++ b/nikola/data/themes/base-jinja/templates/listing.tmpl @@ -1,15 +1,15 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} -{% import 'crumbs.tmpl' as ui with context %} +{% import 'ui_helper.tmpl' as ui with context %} {% block content %} -{{ ui.bar(crumbs) }} +{{ ui.breadcrumbs(crumbs) }} {% if folders or files %} <ul> {% for name in folders %} - <li><a href="{{ name|urlencode }}"><i class="icon-folder-open"></i> {{ name|e }}</a> + <li><a href="{{ name|e }}" class="listing-folder">{{ name|e }}</a> {% endfor %} {% for name in files %} - <li><a href="{{ name|urlencode }}.html"><i class="icon-file"></i> {{ name|e }}</a> + <li><a href="{{ name|e }}.html" class="listing-file">{{ name|e }}</a> {% endfor %} </ul> {% endif %} @@ -22,5 +22,3 @@ {{ code }} {% endif %} {% endblock %} - - diff --git a/nikola/data/themes/base-jinja/templates/math_helper.tmpl b/nikola/data/themes/base-jinja/templates/math_helper.tmpl new file mode 100644 index 0000000..c16f9b8 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/math_helper.tmpl @@ -0,0 +1,69 @@ +{# Note: at present, MathJax and KaTeX do not respect the USE_CDN configuration option #} +{% macro math_scripts() %} + {% if use_katex %} + <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js" integrity="sha384-9Nhn55MVVN0/4OFx7EE5kpFBPsEMZxKTCnA+4fqDmg12eCTqGi6+BB2LjY8brQxJ" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"></script> + {% if katex_auto_render %} + <script> + renderMathInElement(document.body, + { + {{ katex_auto_render }} + } + ); + </script> + {% else %} + <script> + renderMathInElement(document.body, + { + delimiters: [ + {left: "$$", right: "$$", display: true}, + {left: "\\[", right: "\\]", display: true}, + {left: "\\begin{equation*}", right: "\\end{equation*}", display: true}, + {left: "\\(", right: "\\)", display: false} + ] + } + ); + </script> + {% endif %} + {% else %} +{# Note: given the size of MathJax; nikola will retrieve MathJax from a CDN regardless of use_cdn configuration #} + <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML" integrity="sha384-3lJUsx1TJHt7BA4udB5KPnDrlkO8T6J6v/op7ui0BbCjvZ9WqV4Xm6DTP6kQ/iBH" crossorigin="anonymous"></script> + {% if mathjax_config %} + {{ mathjax_config }} + {% else %} + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); + </script> + {% endif %} + {% endif %} +{% endmacro %} + +{% macro math_styles() %} + {% if use_katex %} + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css" integrity="sha384-yFRtMMDnQtDRO8rLpMIKrtPCD5jdktao2TV19YiZYWMDkUR5GQZR/NOVTdquEx1j" crossorigin="anonymous"> + {% endif %} +{% endmacro %} + +{% macro math_scripts_ifpost(post) %} + {% if post.has_math %} + {{ math_scripts() }} + {% endif %} +{% endmacro %} + +{% macro math_scripts_ifposts(posts) %} + {% if posts|selectattr("has_math")|list %} + {{ math_scripts() }} + {% endif %} +{% endmacro %} + +{% macro math_styles_ifpost(post) %} + {% if post.has_math %} + {{ math_styles() }} + {% endif %} +{% endmacro %} + +{% macro math_styles_ifposts(posts) %} + {% if posts|selectattr("has_math")|list %} + {{ math_styles() }} + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/page.tmpl b/nikola/data/themes/base-jinja/templates/page.tmpl new file mode 100644 index 0000000..c2f3f7a --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/page.tmpl @@ -0,0 +1 @@ +{% extends 'story.tmpl' %} diff --git a/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl b/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl new file mode 100644 index 0000000..73cf699 --- /dev/null +++ b/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl @@ -0,0 +1,16 @@ +{# -*- coding: utf-8 -*- #} +{% macro page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5) %} +<div class="page-navigation"> + {% for i, link in enumerate(page_links) %} + {% if (i - current_page)|abs <= surrounding or i == 0 or i == page_links|length - 1 %} + {% if i == current_page %} + <span class="current-page">{{ i+1 }}</span> + {% else %} + <a href="{{ page_links[i] }}">{{ i+1 }}</a> + {% endif %} + {% elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1 %} + <span class="ellipsis">…</span> + {% endif %} + {% endfor %} +</div> +{% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/post.tmpl b/nikola/data/themes/base-jinja/templates/post.tmpl index 5e4d9a5..484a2e0 100644 --- a/nikola/data/themes/base-jinja/templates/post.tmpl +++ b/nikola/data/themes/base-jinja/templates/post.tmpl @@ -2,15 +2,13 @@ {% import 'post_helper.tmpl' as helper with context %} {% import 'post_header.tmpl' as pheader with context %} {% import 'comments_helper.tmpl' as comments with context %} +{% import 'math_helper.tmpl' as math with context %} {% extends 'base.tmpl' %} {% block extra_head %} {{ super() }} {% if post.meta('keywords') %} - <meta name="keywords" content="{{ post.meta('keywords')|e }}"> - {% endif %} - {% if post.description() %} - <meta name="description" content="{{ post.description()|e }}"> + <meta name="keywords" content="{{ smartjoin(', ', post.meta('keywords'))|e }}"> {% endif %} <meta name="author" content="{{ post.author()|e }}"> {% if post.prev_post %} @@ -25,6 +23,7 @@ {{ helper.open_graph_metadata(post) }} {{ helper.twitter_card_information(post) }} {{ helper.meta_translations(post) }} + {{ math.math_styles_ifpost(post) }} {% endblock %} {% block content %} @@ -45,7 +44,7 @@ {{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }} </section> {% endif %} - {{ helper.mathjax_script(post) }} + {{ math.math_scripts_ifpost(post) }} </article> {{ comments.comment_link_script() }} {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/post_header.tmpl b/nikola/data/themes/base-jinja/templates/post_header.tmpl index 6b81120..d3c5d3e 100644 --- a/nikola/data/themes/base-jinja/templates/post_header.tmpl +++ b/nikola/data/themes/base-jinja/templates/post_header.tmpl @@ -23,7 +23,7 @@ {% macro html_sourcelink() %} {% if show_sourcelink %} - <p class="sourceline"><a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a></p> + <p class="sourceline"><a href="{{ post.source_link() }}" class="sourcelink">{{ messages("Source") }}</a></p> {% endif %} {% endmacro %} @@ -31,14 +31,26 @@ <header> {{ html_title() }} <div class="metadata"> - <p class="byline author vcard"><span class="byline-name fn"> - {% if author_pages_generated %} - <a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a> + <p class="byline author vcard p-author h-card"><span class="byline-name fn p-name" itemprop="author"> + {% if author_pages_generated and multiple_authors_per_post %} + {% for author in post.authors() %} + <a class="u-url" href="{{ _link('author', author) }}">{{ author|e }}</a> + {% endfor %} + {% elif author_pages_generated %} + <a class="u-url" href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a> {% else %} {{ post.author()|e }} {% endif %} </span></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)|e }}">{{ post.formatted_date(date_format)|e }}</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)|e }}">{{ post.formatted_date(date_format)|e }}</time> + {% if post.updated and post.updated != post.date %} + <span class="updated"> ({{ messages("updated") }} + <time class="updated dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span> + {% endif %} + </a> + </p> {% if not post.meta('nocomments') and site_has_comments %} <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} {% endif %} @@ -46,9 +58,6 @@ {% if post.meta('link') %} <p class="linkline"><a href="{{ post.meta('link') }}">{{ messages("Original site") }}</a></p> {% endif %} - {% if post.description() %} - <meta name="description" itemprop="description" content="{{ post.description()|e }}"> - {% endif %} </div> {{ html_translations(post) }} </header> diff --git a/nikola/data/themes/base-jinja/templates/post_helper.tmpl b/nikola/data/themes/base-jinja/templates/post_helper.tmpl index e2dcf59..94b3c05 100644 --- a/nikola/data/themes/base-jinja/templates/post_helper.tmpl +++ b/nikola/data/themes/base-jinja/templates/post_helper.tmpl @@ -1,4 +1,5 @@ {# -*- coding: utf-8 -*- #} +{% import 'math_helper.tmpl' as math with context %} {% macro meta_translations(post) %} {% if translations|length > 1 %} @@ -40,31 +41,29 @@ {% endmacro %} {% macro open_graph_metadata(post) %} -{% if use_open_graph %} - <meta property="og:site_name" content="{{ blog_title|e }}"> - <meta property="og:title" content="{{ post.title()[:70]|e }}"> - <meta property="og:url" content="{{ abs_link(permalink) }}"> - {% if post.description() %} +<meta property="og:site_name" content="{{ blog_title|e }}"> +<meta property="og:title" content="{{ post.title()[:70]|e }}"> +<meta property="og:url" content="{{ abs_link(permalink) }}"> +{% if post.description() %} <meta property="og:description" content="{{ post.description()[:200]|e }}"> - {% else %} +{% else %} <meta property="og:description" content="{{ post.text(strip_html=True)[:200]|e }}"> - {% endif %} - {% if post.previewimage %} +{% endif %} +{% if post.previewimage %} <meta property="og:image" content="{{ url_replacer(permalink, post.previewimage, lang, 'absolute') }}"> - {% endif %} - <meta property="og:type" content="article"> +{% endif %} +<meta property="og:type" content="article"> {# Will only work with Pintrest and breaks everywhere else who expect a [Facebook] URI. #} {# %if post.author(): #} {# <meta property="article:author" content="{{ post.author()|e }}"> #} {# %endif #} - {% if post.date.isoformat() %} +{% if post.date.isoformat() %} <meta property="article:published_time" content="{{ post.formatted_date('webiso') }}"> - {% endif %} - {% if post.tags %} - {% for tag in post.tags %} - <meta property="article:tag" content="{{ tag|e }}"> - {% endfor %} - {% endif %} +{% endif %} +{% if post.tags %} + {% for tag in post.tags %} + <meta property="article:tag" content="{{ tag|e }}"> + {% endfor %} {% endif %} {% endmacro %} @@ -84,33 +83,7 @@ {% endif %} {% endmacro %} +{# This function is deprecated; use math_helper directly. #} {% macro mathjax_script(post) %} - {% if post.is_mathjax %} - {% if use_katex %} - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script> - {% if katex_auto_render %} - <script> - renderMathInElement(document.body, - { - {{ katex_auto_render }} - } - ); - </script> - {% else %} - <script> - renderMathInElement(document.body); - </script> - {% endif %} - {% else %} - <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script> - {% if mathjax_config %} - {{ mathjax_config }} - {% else %} - <script type="text/x-mathjax-config"> - MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); - </script> - {% endif %} - {% endif %} - {% endif %} + {{ math.math_scripts_ifpost(post) }} {% endmacro %} diff --git a/nikola/data/themes/base-jinja/templates/sectionindex.tmpl b/nikola/data/themes/base-jinja/templates/sectionindex.tmpl deleted file mode 100644 index f1d3d5b..0000000 --- a/nikola/data/themes/base-jinja/templates/sectionindex.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -{# -*- coding: utf-8 -*- #} -{% extends 'index.tmpl' %} - -{% block extra_head %} - {{ super() }} - {% if generate_atom %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ posts[0].section_name()|e }} section" href="{{ _link('section_index_atom', posts[0].section_slug()) }}"> - {% endif %} -{% endblock %} - -{% block content %} -<div class="sectionindex"> - <header> - <h2><a href="{{ _link('section_index', posts[0].section_slug()) }}">{{ title|e }}</a></h2> - {% if generate_atom %} - <p class="feedlink"><a href="{{ _link('section_index_atom', posts[0].section_slug()) }}" type="application/atom+xml">{{ messages('Updates') }}</a></p> - {% endif %} - </header> - {{ super() }} -</div> -{% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/slides.tmpl b/nikola/data/themes/base-jinja/templates/slides.tmpl deleted file mode 100644 index 0ae8fe8..0000000 --- a/nikola/data/themes/base-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/base-jinja/templates/story.tmpl b/nikola/data/themes/base-jinja/templates/story.tmpl index 1269724..5c93256 100644 --- a/nikola/data/themes/base-jinja/templates/story.tmpl +++ b/nikola/data/themes/base-jinja/templates/story.tmpl @@ -2,6 +2,7 @@ {% import 'post_helper.tmpl' as helper with context %} {% import 'post_header.tmpl' as pheader with context %} {% import 'comments_helper.tmpl' as comments with context %} +{% import 'math_helper.tmpl' as math with context %} {% extends 'post.tmpl' %} {% block content %} @@ -19,6 +20,6 @@ {{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }} </section> {% endif %} - {{ helper.mathjax_script(post) }} + {{ math.math_scripts_ifpost(post) }} </article> {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/tag.tmpl b/nikola/data/themes/base-jinja/templates/tag.tmpl index 363019b..fd5acbf 100644 --- a/nikola/data/themes/base-jinja/templates/tag.tmpl +++ b/nikola/data/themes/base-jinja/templates/tag.tmpl @@ -1,24 +1,17 @@ {# -*- coding: utf-8 -*- #} {% extends 'list_post.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% block extra_head %} - {{ super() }} - {% if translations|length > 1 and generate_rss %} - {% for language in translations|sort %} - <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ tag|e }} ({{ language }})" href="{{ _link(kind + "_rss", tag, language) }}"> - {% endfor %} - {% elif generate_rss %} - <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ tag|e }}" href="{{ _link(kind + "_rss", tag) }}"> - {% endif %} + {{ feeds_translations.head(tag, kind, rss_override=False) }} {% endblock %} - {% block content %} <article class="tagpage"> <header> <h1>{{ title|e }}</h1> {% if description %} - <p>{{ description }}</p> + <p>{{ description }}</p> {% endif %} {% if subcategories %} {{ messages('Subcategories:') }} @@ -29,23 +22,16 @@ </ul> {% endif %} <div class="metadata"> - {% if translations|length > 1 and generate_rss %} - {% 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> - {% endfor %} - {% elif generate_rss %} - <p class="feedlink"><a href="{{ _link(kind + "_rss", tag) }}" type="application/rss+xml">{{ messages('RSS feed') }}</a></p> - {% endif %} + {{ feeds_translations.feed_link(tag, kind=kind) }} </div> + {{ feeds_translations.translation_link(kind) }} </header> {% if posts %} - <ul class="postlist"> - {% for post in posts %} - <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}<a></li> - {% endfor %} - </ul> + <ul class="postlist"> + {% for post in posts %} + <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}<a></li> + {% endfor %} + </ul> {% endif %} </article> {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/tagindex.tmpl b/nikola/data/themes/base-jinja/templates/tagindex.tmpl index 624961d..8ea6a4b 100644 --- a/nikola/data/themes/base-jinja/templates/tagindex.tmpl +++ b/nikola/data/themes/base-jinja/templates/tagindex.tmpl @@ -1,5 +1,6 @@ {# -*- coding: utf-8 -*- #} {% extends 'index.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} {% block content_header %} <header> @@ -15,16 +16,14 @@ {% endfor %} </ul> {% endif %} + <div class="metadata"> + {{ feeds_translations.feed_link(tag, kind) }} + {{ feeds_translations.translation_link(kind) }} + </div> </header> {% endblock %} {% block extra_head %} {{ super() }} - {% if translations|length > 1 and generate_atom %} - {% for language in translations|sort %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag|e }} section ({{ language }})" href="{{ _link(kind + "_atom", tag, language) }}"> - {% endfor %} - {% elif generate_atom %} - <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag|e }} section" href="{{ _link("tag" + "_atom", tag) }}"> - {% endif %} + {{ feeds_translations.head(tag, kind, rss_override=False) }} {% endblock %} diff --git a/nikola/data/themes/base-jinja/templates/tags.tmpl b/nikola/data/themes/base-jinja/templates/tags.tmpl index 936ed21..931ee71 100644 --- a/nikola/data/themes/base-jinja/templates/tags.tmpl +++ b/nikola/data/themes/base-jinja/templates/tags.tmpl @@ -1,10 +1,18 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block extra_head %} + {{ feeds_translations.head(kind=kind, feeds=False) }} +{% endblock %} {% block content %} <article class="tagindex"> <header> <h1>{{ title|e }}</h1> + <div class="metadata"> + {{ feeds_translations.translation_link(kind) }} + </div> </header> {% if cat_items %} {% if items %} diff --git a/nikola/data/themes/base-jinja/templates/crumbs.tmpl b/nikola/data/themes/base-jinja/templates/ui_helper.tmpl index 970d509..9eb5697 100644 --- a/nikola/data/themes/base-jinja/templates/crumbs.tmpl +++ b/nikola/data/themes/base-jinja/templates/ui_helper.tmpl @@ -1,6 +1,5 @@ {# -*- coding: utf-8 -*- #} - -{% macro bar(crumbs) %} +{% macro breadcrumbs(crumbs) %} {% if crumbs %} <nav class="breadcrumbs"> <ul class="breadcrumb"> diff --git a/nikola/data/themes/base/assets/css/baguetteBox.min.css b/nikola/data/themes/base/assets/css/baguetteBox.min.css new file mode 120000 index 0000000..601d72f --- /dev/null +++ b/nikola/data/themes/base/assets/css/baguetteBox.min.css @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.css
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/css/html4css1.css b/nikola/data/themes/base/assets/css/html4css1.css new file mode 100644 index 0000000..cc29335 --- /dev/null +++ b/nikola/data/themes/base/assets/css/html4css1.css @@ -0,0 +1 @@ +@import url("rst_base.css"); diff --git a/nikola/data/themes/base/assets/css/ipython.min.css b/nikola/data/themes/base/assets/css/ipython.min.css index f9934c2..c1c6bc4 100644 --- a/nikola/data/themes/base/assets/css/ipython.min.css +++ b/nikola/data/themes/base/assets/css/ipython.min.css @@ -2,8 +2,8 @@ * * IPython base * -*/.modal.fade .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}code{color:#000}pre{font-size:inherit;line-height:inherit}label{font-weight:normal}.border-box-sizing{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.corner-all{border-radius:2px}.no-padding{padding:0}.hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.hbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.vbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.hbox.reverse,.vbox.reverse,.reverse{-webkit-box-direction:reverse;-moz-box-direction:reverse;box-direction:reverse;flex-direction:row-reverse}.hbox.box-flex0,.vbox.box-flex0,.box-flex0{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none;width:auto}.hbox.box-flex1,.vbox.box-flex1,.box-flex1{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex,.vbox.box-flex,.box-flex{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex2,.vbox.box-flex2,.box-flex2{-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2}.box-group1{-webkit-box-flex-group:1;-moz-box-flex-group:1;box-flex-group:1}.box-group2{-webkit-box-flex-group:2;-moz-box-flex-group:2;box-flex-group:2}.hbox.start,.vbox.start,.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;justify-content:flex-start}.hbox.end,.vbox.end,.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;justify-content:flex-end}.hbox.center,.vbox.center,.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;justify-content:center}.hbox.baseline,.vbox.baseline,.baseline{-webkit-box-pack:baseline;-moz-box-pack:baseline;box-pack:baseline;justify-content:baseline}.hbox.stretch,.vbox.stretch,.stretch{-webkit-box-pack:stretch;-moz-box-pack:stretch;box-pack:stretch;justify-content:stretch}.hbox.align-start,.vbox.align-start,.align-start{-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.hbox.align-end,.vbox.align-end,.align-end{-webkit-box-align:end;-moz-box-align:end;box-align:end;align-items:flex-end}.hbox.align-center,.vbox.align-center,.align-center{-webkit-box-align:center;-moz-box-align:center;box-align:center;align-items:center}.hbox.align-baseline,.vbox.align-baseline,.align-baseline{-webkit-box-align:baseline;-moz-box-align:baseline;box-align:baseline;align-items:baseline}.hbox.align-stretch,.vbox.align-stretch,.align-stretch{-webkit-box-align:stretch;-moz-box-align:stretch;box-align:stretch;align-items:stretch}div.error{margin:2em;text-align:center}div.error>h1{font-size:500%;line-height:normal}div.error>p{font-size:200%;line-height:normal}div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}/*! +*/.modal.fade .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}code{color:#000}pre{font-size:inherit;line-height:inherit}label{font-weight:normal}.border-box-sizing{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.corner-all{border-radius:2px}.no-padding{padding:0}.hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.hbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.vbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.hbox.reverse,.vbox.reverse,.reverse{-webkit-box-direction:reverse;-moz-box-direction:reverse;box-direction:reverse;flex-direction:row-reverse}.hbox.box-flex0,.vbox.box-flex0,.box-flex0{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none;width:auto}.hbox.box-flex1,.vbox.box-flex1,.box-flex1{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex,.vbox.box-flex,.box-flex{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex2,.vbox.box-flex2,.box-flex2{-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2}.box-group1{-webkit-box-flex-group:1;-moz-box-flex-group:1;box-flex-group:1}.box-group2{-webkit-box-flex-group:2;-moz-box-flex-group:2;box-flex-group:2}.hbox.start,.vbox.start,.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;justify-content:flex-start}.hbox.end,.vbox.end,.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;justify-content:flex-end}.hbox.center,.vbox.center,.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;justify-content:center}.hbox.baseline,.vbox.baseline,.baseline{-webkit-box-pack:baseline;-moz-box-pack:baseline;box-pack:baseline;justify-content:baseline}.hbox.stretch,.vbox.stretch,.stretch{-webkit-box-pack:stretch;-moz-box-pack:stretch;box-pack:stretch;justify-content:stretch}.hbox.align-start,.vbox.align-start,.align-start{-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.hbox.align-end,.vbox.align-end,.align-end{-webkit-box-align:end;-moz-box-align:end;box-align:end;align-items:flex-end}.hbox.align-center,.vbox.align-center,.align-center{-webkit-box-align:center;-moz-box-align:center;box-align:center;align-items:center}.hbox.align-baseline,.vbox.align-baseline,.align-baseline{-webkit-box-align:baseline;-moz-box-align:baseline;box-align:baseline;align-items:baseline}.hbox.align-stretch,.vbox.align-stretch,.align-stretch{-webkit-box-align:stretch;-moz-box-align:stretch;box-align:stretch;align-items:stretch}div.error{margin:2em;text-align:center}div.error>h1{font-size:500%;line-height:normal}div.error>p{font-size:200%;line-height:normal}div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}div.traceback-wrapper pre.traceback{max-height:600px;overflow:auto}/*! * * IPython notebook * -*/.ansibold{font-weight:bold}.ansiblack{color:black}.ansired{color:darkred}.ansigreen{color:darkgreen}.ansiyellow{color:#c4a000}.ansiblue{color:darkblue}.ansipurple{color:darkviolet}.ansicyan{color:steelblue}.ansigray{color:gray}.ansibgblack{background-color:black}.ansibgred{background-color:red}.ansibggreen{background-color:green}.ansibgyellow{background-color:yellow}.ansibgblue{background-color:blue}.ansibgpurple{background-color:magenta}.ansibgcyan{background-color:cyan}.ansibggray{background-color:gray}div.cell{border:1px solid transparent;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;border-radius:2px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:thin;border-style:solid;width:100%;padding:5px;margin:0;outline:none}div.cell.selected{border-color:#ababab}@media print{div.cell.selected{border-color:transparent}}.edit_mode div.cell.selected{border-color:green}@media print{.edit_mode div.cell.selected{border-color:transparent}}.prompt{min-width:14ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em}@media (max-width:540px){.prompt{text-align:left}}div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}@-moz-document url-prefix(){div.inner_cell{overflow-x:hidden}}div.input_area{border:1px solid #cfcfcf;border-radius:2px;background:#f7f7f7;line-height:1.21429em}div.prompt:empty{padding-top:0;padding-bottom:0}div.unrecognized_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.unrecognized_cell .inner_cell{border-radius:2px;padding:5px;font-weight:bold;color:red;border:1px solid #cfcfcf;background:#eaeaea}div.unrecognized_cell .inner_cell a{color:inherit;text-decoration:none}div.unrecognized_cell .inner_cell a:hover{color:inherit;text-decoration:none}@media (max-width:540px){div.unrecognized_cell>div.prompt{display:none}}@media print{div.code_cell{page-break-inside:avoid}}div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media (max-width:540px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:navy;border-top:1px solid transparent}div.input_area>div.highlight{margin:.4em;border:none;padding:0;background-color:transparent}div.input_area>div.highlight>pre{margin:0;border:none;padding:0;background-color:transparent}.CodeMirror{line-height:1.21429em;font-size:14px;height:auto;background:none}.CodeMirror-scroll{overflow-y:hidden;overflow-x:auto}.CodeMirror-lines{padding:.4em}.CodeMirror-linenumber{padding:0 8px 0 4px}.CodeMirror-gutters{border-bottom-left-radius:2px;border-top-left-radius:2px}.CodeMirror pre{padding:0;border:0;border-radius:0}.highlight-base{color:#000}.highlight-variable{color:#000}.highlight-variable-2{color:#1a1a1a}.highlight-variable-3{color:#333}.highlight-string{color:#ba2121}.highlight-comment{color:#408080;font-style:italic}.highlight-number{color:#080}.highlight-atom{color:#88f}.highlight-keyword{color:#008000;font-weight:bold}.highlight-builtin{color:#008000}.highlight-error{color:#f00}.highlight-operator{color:#a2f;font-weight:bold}.highlight-meta{color:#a2f}.highlight-def{color:#00f}.highlight-string-2{color:#f50}.highlight-qualifier{color:#555}.highlight-bracket{color:#997}.highlight-tag{color:#170}.highlight-attribute{color:#00c}.highlight-header{color:blue}.highlight-quote{color:#090}.highlight-link{color:#00c}.cm-s-ipython span.cm-keyword{color:#008000;font-weight:bold}.cm-s-ipython span.cm-atom{color:#88f}.cm-s-ipython span.cm-number{color:#080}.cm-s-ipython span.cm-def{color:#00f}.cm-s-ipython span.cm-variable{color:#000}.cm-s-ipython span.cm-operator{color:#a2f;font-weight:bold}.cm-s-ipython span.cm-variable-2{color:#1a1a1a}.cm-s-ipython span.cm-variable-3{color:#333}.cm-s-ipython span.cm-comment{color:#408080;font-style:italic}.cm-s-ipython span.cm-string{color:#ba2121}.cm-s-ipython span.cm-string-2{color:#f50}.cm-s-ipython span.cm-meta{color:#a2f}.cm-s-ipython span.cm-qualifier{color:#555}.cm-s-ipython span.cm-builtin{color:#008000}.cm-s-ipython span.cm-bracket{color:#997}.cm-s-ipython span.cm-tag{color:#170}.cm-s-ipython span.cm-attribute{color:#00c}.cm-s-ipython span.cm-header{color:blue}.cm-s-ipython span.cm-quote{color:#090}.cm-s-ipython span.cm-link{color:#00c}.cm-s-ipython span.cm-error{color:#f00}.cm-s-ipython span.cm-tab{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);background-position:right;background-repeat:no-repeat}div.output_wrapper{position:relative;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;z-index:1}div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:2px;-webkit-box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);display:block}div.output_collapsed{margin:0;padding:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}div.out_prompt_overlay{height:100%;padding:0 .4em;position:absolute;border-radius:2px}div.out_prompt_overlay:hover{-webkit-box-shadow:inset 0 0 1px #000;box-shadow:inset 0 0 1px #000;background:rgba(240,240,240,0.5)}div.output_prompt{color:darkred}div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.output_area .MathJax_Display{text-align:left !important}div.output_area .rendered_html table{margin-left:0;margin-right:0}div.output_area .rendered_html img{margin-left:0;margin-right:0}div.output_area img,div.output_area svg{max-width:100%;height:auto}div.output_area img.unconfined,div.output_area svg.unconfined{max-width:none}.output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}@media (max-width:540px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;vertical-align:baseline;color:black;background-color:transparent;border-radius:0}div.output_subarea{overflow-x:auto;padding:.4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1;max-width:calc(100% - 14ex)}div.output_text{text-align:left;color:#000;line-height:1.21429em}div.output_stderr{background:#fdd}div.output_latex{text-align:left}div.output_javascript:empty{padding:0}.js-error{color:darkred}div.raw_input_container{font-family:monospace;padding-top:5px}input.raw_input{font-family:inherit;font-size:inherit;color:inherit;width:auto;vertical-align:baseline;padding:0 .25em;margin:0 .25em}input.raw_input:focus{box-shadow:none}p.p-space{margin-bottom:10px}div.output_unrecognized{padding:5px;font-weight:bold;color:red}div.output_unrecognized a{color:inherit;text-decoration:none}div.output_unrecognized a:hover{color:inherit;text-decoration:none}.rendered_html{color:#000}.rendered_html em{font-style:italic}.rendered_html strong{font-weight:bold}.rendered_html u{text-decoration:underline}.rendered_html :link{text-decoration:underline}.rendered_html :visited{text-decoration:underline}.rendered_html h1{font-size:185.7%;margin:1.08em 0 0 0;font-weight:bold;line-height:1}.rendered_html h2{font-size:157.1%;margin:1.27em 0 0 0;font-weight:bold;line-height:1}.rendered_html h3{font-size:128.6%;margin:1.55em 0 0 0;font-weight:bold;line-height:1}.rendered_html h4{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1}.rendered_html h5{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1;font-style:italic}.rendered_html h6{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1;font-style:italic}.rendered_html h1:first-child{margin-top:.538em}.rendered_html h2:first-child{margin-top:.636em}.rendered_html h3:first-child{margin-top:.777em}.rendered_html h4:first-child{margin-top:1em}.rendered_html h5:first-child{margin-top:1em}.rendered_html h6:first-child{margin-top:1em}.rendered_html ul{list-style:disc;margin:0 2em;padding-left:0}.rendered_html ul ul{list-style:square;margin:0 2em}.rendered_html ul ul ul{list-style:circle;margin:0 2em}.rendered_html ol{list-style:decimal;margin:0 2em;padding-left:0}.rendered_html ol ol{list-style:upper-alpha;margin:0 2em}.rendered_html ol ol ol{list-style:lower-alpha;margin:0 2em}.rendered_html ol ol ol ol{list-style:lower-roman;margin:0 2em}.rendered_html ol ol ol ol ol{list-style:decimal;margin:0 2em}.rendered_html *+ul{margin-top:1em}.rendered_html *+ol{margin-top:1em}.rendered_html hr{color:black;background-color:black}.rendered_html pre{margin:1em 2em}.rendered_html pre,.rendered_html code{border:0;background-color:#fff;color:#000;font-size:100%;padding:0}.rendered_html blockquote{margin:1em 2em}.rendered_html table{margin-left:auto;margin-right:auto;border:1px solid black;border-collapse:collapse}.rendered_html tr,.rendered_html th,.rendered_html td{border:1px solid black;border-collapse:collapse;margin:1em 2em}.rendered_html td,.rendered_html th{text-align:left;vertical-align:middle;padding:4px}.rendered_html th{font-weight:bold}.rendered_html *+table{margin-top:1em}.rendered_html p{text-align:left}.rendered_html *+p{margin-top:1em}.rendered_html img{display:block;margin-left:auto;margin-right:auto}.rendered_html *+img{margin-top:1em}.rendered_html img,.rendered_html svg{max-width:100%;height:auto}.rendered_html img.unconfined,.rendered_html svg.unconfined{max-width:none}div.text_cell{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media (max-width:540px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden}h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible}.text_cell.rendered .input_area{display:none}.text_cell.rendered .rendered_html{overflow-x:auto}.text_cell.unrendered .text_cell_render{display:none}.cm-header-1,.cm-header-2,.cm-header-3,.cm-header-4,.cm-header-5,.cm-header-6{font-weight:bold;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.cm-header-1{font-size:185.7%}.cm-header-2{font-size:157.1%}.cm-header-3{font-size:128.6%}.cm-header-4{font-size:110%}.cm-header-5{font-size:100%;font-style:italic}.cm-header-6{font-size:100%;font-style:italic}.widget-interact>div,.widget-interact>input{padding:2.5px}.widget-area{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-area .widget-subarea{padding:.44em .4em .4em 1px;margin-left:6px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2;-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.widget-area.connection-problems .prompt:after{content:"\f127";font-family:'FontAwesome';color:#d9534f;font-size:14px;top:3px;padding:3px}.slide-track{border:1px solid #ccc;background:#fff;border-radius:2px}.widget-hslider{padding-left:8px;padding-right:2px;overflow:visible;width:350px;height:5px;max-height:5px;margin-top:13px;margin-bottom:10px;border:1px solid #ccc;background:#fff;border-radius:2px;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-hslider .ui-slider{border:0;background:none;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.widget-hslider .ui-slider .ui-slider-handle{width:12px;height:28px;margin-top:-8px;border-radius:2px}.widget-hslider .ui-slider .ui-slider-range{height:12px;margin-top:-4px;background:#eee}.widget-vslider{padding-bottom:5px;overflow:visible;width:5px;max-width:5px;height:250px;margin-left:12px;border:1px solid #ccc;background:#fff;border-radius:2px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.widget-vslider .ui-slider{border:0;background:none;margin-left:-4px;margin-top:5px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.widget-vslider .ui-slider .ui-slider-handle{width:28px;height:12px;margin-left:-9px;border-radius:2px}.widget-vslider .ui-slider .ui-slider-range{width:12px;margin-left:-1px;background:#eee}.widget-text{width:350px;margin:0}.widget-listbox{width:350px;margin-bottom:0}.widget-numeric-text{width:150px;margin:0}.widget-progress{margin-top:6px;min-width:350px}.widget-progress .progress-bar{-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none}.widget-combo-btn{min-width:125px}.widget_item .dropdown-menu li a{color:inherit}.widget-hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-hbox input[type="checkbox"]{margin-top:9px;margin-bottom:10px}.widget-hbox .widget-label{min-width:10ex;padding-right:8px;padding-top:5px;text-align:right;vertical-align:text-top}.widget-hbox .widget-readout{padding-left:8px;padding-top:5px;text-align:left;vertical-align:text-top}.widget-vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.widget-vbox .widget-label{padding-bottom:5px;text-align:center;vertical-align:text-bottom}.widget-vbox .widget-readout{padding-top:5px;text-align:center;vertical-align:text-top}.widget-box{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.widget-radio-box{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding-top:4px}.widget-radio-box label{margin-top:0;margin-left:20px}/*# sourceMappingURL=ipython.min.css.map */
\ No newline at end of file +*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-fg{color:#282c36}.ansi-black-intense-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-fg{color:#b22b31}.ansi-red-intense-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-fg{color:#007427}.ansi-green-intense-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-fg{color:#b27d12}.ansi-yellow-intense-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-fg{color:#0065ca}.ansi-blue-intense-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-fg{color:#a03196}.ansi-magenta-intense-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-fg{color:#258f8f}.ansi-cyan-intense-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-fg{color:#a1a6b2}.ansi-white-intense-bg{background-color:#a1a6b2}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}.ansi-inverse{outline:.5px dotted}.ansibold{font-weight:bold}.ansiblack{color:black}.ansired{color:darkred}.ansigreen{color:darkgreen}.ansiyellow{color:#c4a000}.ansiblue{color:darkblue}.ansipurple{color:darkviolet}.ansicyan{color:steelblue}.ansigray{color:gray}.ansibgblack{background-color:black}.ansibgred{background-color:red}.ansibggreen{background-color:green}.ansibgyellow{background-color:yellow}.ansibgblue{background-color:blue}.ansibgpurple{background-color:magenta}.ansibgcyan{background-color:cyan}.ansibggray{background-color:gray}div.cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;border-radius:2px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:1px;border-style:solid;border-color:transparent;width:100%;padding:5px;margin:0;outline:0;position:relative;overflow:visible}div.cell:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:transparent}div.cell.jupyter-soft-selected{border-left-color:#e3f2fd;border-left-width:1px;padding-left:5px;border-right-color:#e3f2fd;border-right-width:1px;background:#e3f2fd}@media print{div.cell.jupyter-soft-selected{border-color:transparent}}div.cell.selected,div.cell.selected.jupyter-soft-selected{border-color:#ababab}div.cell.selected:before,div.cell.selected.jupyter-soft-selected:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:#42a5f5}@media print{div.cell.selected,div.cell.selected.jupyter-soft-selected{border-color:transparent}}.edit_mode div.cell.selected{border-color:#66bb6a}.edit_mode div.cell.selected:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:#66bb6a}@media print{.edit_mode div.cell.selected{border-color:transparent}}.prompt{min-width:14ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@media(max-width:540px){.prompt{text-align:left}}div.inner_cell{min-width:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}div.input_area>div.highlight>pre{border:1px solid #cfcfcf;line-height:1.21429em}div.prompt:empty{padding-top:0;padding-bottom:0}div.unrecognized_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.unrecognized_cell .inner_cell{border-radius:2px;padding:5px;font-weight:bold;color:red;border:1px solid #cfcfcf;background:#eaeaea}div.unrecognized_cell .inner_cell a{color:inherit;text-decoration:none}div.unrecognized_cell .inner_cell a:hover{color:inherit;text-decoration:none}@media(max-width:540px){div.unrecognized_cell>div.prompt{display:none}}@media print{div.code_cell{page-break-inside:avoid}}div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media(max-width:540px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:#303f9f;border-top:1px solid transparent}div.input_area>div.highlight{margin:0;border:0;padding:0;background-color:transparent}div.input_area>div.highlight>pre{margin:0;border:0;padding:.4em}.CodeMirror{line-height:1.21429em;font-size:14px;height:auto;background:0}.CodeMirror-scroll{overflow-y:hidden;overflow-x:auto}.CodeMirror-lines{padding:.4em}.CodeMirror-linenumber{padding:0 8px 0 4px}.CodeMirror-gutters{border-bottom-left-radius:2px;border-top-left-radius:2px}.CodeMirror pre{padding:0;border:0;border-radius:0}.highlight-base{color:#000}.highlight-variable{color:#000}.highlight-variable-2{color:#1a1a1a}.highlight-variable-3{color:#333}.highlight-string{color:#ba2121}.highlight-comment{color:#408080;font-style:italic}.highlight-number{color:#080}.highlight-atom{color:#88F}.highlight-keyword{color:#008000;font-weight:bold}.highlight-builtin{color:#008000}.highlight-error{color:red}.highlight-operator{color:#a2f;font-weight:bold}.highlight-meta{color:#a2f}.highlight-def{color:#00f}.highlight-string-2{color:#f50}.highlight-qualifier{color:#555}.highlight-bracket{color:#997}.highlight-tag{color:#170}.highlight-attribute{color:#00c}.highlight-header{color:blue}.highlight-quote{color:#090}.highlight-link{color:#00c}.cm-s-ipython span.cm-keyword{color:#008000;font-weight:bold}.cm-s-ipython span.cm-atom{color:#88F}.cm-s-ipython span.cm-number{color:#080}.cm-s-ipython span.cm-def{color:#00f}.cm-s-ipython span.cm-variable{color:#000}.cm-s-ipython span.cm-operator{color:#a2f;font-weight:bold}.cm-s-ipython span.cm-variable-2{color:#1a1a1a}.cm-s-ipython span.cm-variable-3{color:#333}.cm-s-ipython span.cm-comment{color:#408080;font-style:italic}.cm-s-ipython span.cm-string{color:#ba2121}.cm-s-ipython span.cm-string-2{color:#f50}.cm-s-ipython span.cm-meta{color:#a2f}.cm-s-ipython span.cm-qualifier{color:#555}.cm-s-ipython span.cm-builtin{color:#008000}.cm-s-ipython span.cm-bracket{color:#997}.cm-s-ipython span.cm-tag{color:#170}.cm-s-ipython span.cm-attribute{color:#00c}.cm-s-ipython span.cm-header{color:blue}.cm-s-ipython span.cm-quote{color:#090}.cm-s-ipython span.cm-link{color:#00c}.cm-s-ipython span.cm-error{color:red}.cm-s-ipython span.cm-tab{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);background-position:right;background-repeat:no-repeat}div.output_wrapper{position:relative;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;z-index:1}div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:2px;-webkit-box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);display:block}div.output_collapsed{margin:0;padding:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}div.out_prompt_overlay{height:100%;padding:0 .4em;position:absolute;border-radius:2px}div.out_prompt_overlay:hover{-webkit-box-shadow:inset 0 0 1px #000;box-shadow:inset 0 0 1px #000;background:rgba(240,240,240,0.5)}div.output_prompt{color:#d84315}div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.output_area .MathJax_Display{text-align:left !important}div.output_area .rendered_html table{margin-left:0;margin-right:0}div.output_area .rendered_html img{margin-left:0;margin-right:0}div.output_area img,div.output_area svg{max-width:100%;height:auto}div.output_area img.unconfined,div.output_area svg.unconfined{max-width:none}div.output_area .mglyph>img{max-width:none}.output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}@media(max-width:540px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;vertical-align:baseline;color:black;background-color:transparent;border-radius:0}div.output_subarea{overflow-x:auto;padding:.4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1;max-width:calc(100% - 14ex)}div.output_scroll div.output_subarea{overflow-x:visible}div.output_text{text-align:left;color:#000;line-height:1.21429em}div.output_stderr{background:#fdd}div.output_latex{text-align:left}div.output_javascript:empty{padding:0}.js-error{color:darkred}div.raw_input_container{line-height:1.21429em;padding-top:5px}input.raw_input{font-family:monospace;font-size:inherit;color:inherit;width:auto;vertical-align:baseline;padding:0 .25em;margin:0 .25em}input.raw_input:focus{box-shadow:none}p.p-space{margin-bottom:10px}div.output_unrecognized{padding:5px;font-weight:bold;color:red}div.output_unrecognized a{color:inherit;text-decoration:none}div.output_unrecognized a:hover{color:inherit;text-decoration:none}.rendered_html{color:#000}.rendered_html em{font-style:italic}.rendered_html strong{font-weight:bold}.rendered_html u{text-decoration:underline}.rendered_html :link{text-decoration:underline}.rendered_html :visited{text-decoration:underline}.rendered_html h1{font-size:185.7%;margin:1.08em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h2{font-size:157.1%;margin:1.27em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h3{font-size:128.6%;margin:1.55em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h4{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h5{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0;font-style:italic}.rendered_html h6{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0;font-style:italic}.rendered_html h1:first-child{margin-top:.538em}.rendered_html h2:first-child{margin-top:.636em}.rendered_html h3:first-child{margin-top:.777em}.rendered_html h4:first-child{margin-top:1em}.rendered_html h5:first-child{margin-top:1em}.rendered_html h6:first-child{margin-top:1em}.rendered_html ul:not(.list-inline),.rendered_html ol:not(.list-inline){padding-left:2em}.rendered_html ul{list-style:disc}.rendered_html ul ul{list-style:square}.rendered_html ul ul ul{list-style:circle}.rendered_html ol{list-style:decimal}.rendered_html ol ol{list-style:upper-alpha}.rendered_html ol ol ol{list-style:lower-alpha}.rendered_html ol ol ol ol{list-style:lower-roman}.rendered_html ol ol ol ol ol{list-style:decimal}.rendered_html *+ul{margin-top:1em}.rendered_html *+ol{margin-top:1em}.rendered_html hr{color:black;background-color:black}.rendered_html pre{margin:1em 2em}.rendered_html pre,.rendered_html code{border:0;background-color:#fff;color:#000;font-size:100%;padding:0}.rendered_html blockquote{margin:1em 2em}.rendered_html table{margin-left:auto;margin-right:auto;border:0;border-collapse:collapse;border-spacing:0;color:black;font-size:12px;table-layout:fixed}.rendered_html thead{border-bottom:1px solid black;vertical-align:bottom}.rendered_html tr,.rendered_html th,.rendered_html td{text-align:right;vertical-align:middle;padding:.5em .5em;line-height:normal;white-space:normal;max-width:none;border:0}.rendered_html th{font-weight:bold}.rendered_html tbody tr:nth-child(odd){background:#f5f5f5}.rendered_html tbody tr:hover{background:rgba(66,165,245,0.2)}.rendered_html *+table{margin-top:1em}.rendered_html p{text-align:left}.rendered_html *+p{margin-top:1em}.rendered_html img{display:block;margin-left:auto;margin-right:auto}.rendered_html *+img{margin-top:1em}.rendered_html img,.rendered_html svg{max-width:100%;height:auto}.rendered_html img.unconfined,.rendered_html svg.unconfined{max-width:none}.rendered_html .alert{margin-bottom:initial}.rendered_html *+.alert{margin-top:1em}div.text_cell{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media(max-width:540px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:0;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden}h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible}.text_cell.rendered .input_area{display:none}.text_cell.rendered .rendered_html{overflow-x:auto;overflow-y:hidden}.text_cell.rendered .rendered_html tr,.text_cell.rendered .rendered_html th,.text_cell.rendered .rendered_html td{max-width:none}.text_cell.unrendered .text_cell_render{display:none}.text_cell .dropzone .input_area{border:2px dashed #bababa;margin:-1px}.cm-header-1,.cm-header-2,.cm-header-3,.cm-header-4,.cm-header-5,.cm-header-6{font-weight:bold;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.cm-header-1{font-size:185.7%}.cm-header-2{font-size:157.1%}.cm-header-3{font-size:128.6%}.cm-header-4{font-size:110%}.cm-header-5{font-size:100%;font-style:italic}.cm-header-6{font-size:100%;font-style:italic} diff --git a/nikola/data/themes/base/assets/css/ipython.min.css.map b/nikola/data/themes/base/assets/css/ipython.min.css.map deleted file mode 100644 index 3e36e5e..0000000 --- a/nikola/data/themes/base/assets/css/ipython.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../base/less/variables.less","../components/bootstrap/less/mixins/vendor-prefixes.less","../base/less/mixins.less","../base/less/flexbox.less","../base/less/error.less","../notebook/less/ansicolors.less","../notebook/less/cell.less","../notebook/less/codecell.less","../notebook/less/codemirror.less","../notebook/less/highlight.less","../components/codemirror/lib/codemirror.css","../notebook/less/outputarea.less","../notebook/less/renderedhtml.less","../notebook/less/textcell.less","../components/bootstrap/less/variables.less","../widgets/less/widgets.less","../components/font-awesome/less/variables.less"],"names":[],"mappings":";;;;EAqBE,MAAC,KAAM,eCyHP,kBAAmB,eAAnB,CACI,cAAe,eAAf,CACC,aAAc,eAAd,CACG,UAAW,gBDtHrB,KACE,WAIF,IAGE,iBAAA,CACA,oBAIF,MACI,mBEvCJ,mBACI,qBAAA,CACA,0BAAA,CACA,8BAGJ,YACI,kBAOJ,YACI,UCGJ,MAEI,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBAGJ,KAAM,GAEF,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,UAGJ,MAEI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBAGJ,KAAM,GAEF,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,UAGJ,KAAK,SACL,KAAK,SACL,SAEI,6BAAA,CACA,0BAAA,CACA,qBAAA,CAGA,2BAGJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,SAAA,CACA,WAGJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAGJ,KAAK,UACL,KAAK,UACL,UAVI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAUJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAGJ,YAEI,wBAAA,CACA,qBAAA,CACA,iBAGJ,YAEI,wBAAA,CACA,qBAAA,CACA,iBAGJ,KAAK,OACL,KAAK,OACL,OAEI,sBAAA,CACA,mBAAA,CACA,cAAA,CAGA,2BAGJ,KAAK,KACL,KAAK,KACL,KAEI,oBAAA,CACA,iBAAA,CACA,YAAA,CAGA,yBAGJ,KAAK,QACL,KAAK,QACL,QAEI,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBAGJ,KAAK,UACL,KAAK,UACL,UAEI,yBAAA,CACA,sBAAA,CACA,iBAAA,CAGA,yBAGJ,KAAK,SACL,KAAK,SACL,SAEI,wBAAA,CACA,qBAAA,CACA,gBAAA,CAGA,wBAGJ,KAAK,aACL,KAAK,aACL,aAEI,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBAGJ,KAAK,WACL,KAAK,WACL,WAEI,qBAAA,CACA,kBAAA,CACA,aAAA,CAGA,qBAGJ,KAAK,cACL,KAAK,cACL,cAEI,wBAAA,CACA,qBAAA,CACA,gBAAA,CAGA,mBAGJ,KAAK,gBACL,KAAK,gBACL,gBAEI,0BAAA,CACA,uBAAA,CACA,kBAAA,CAGA,qBAGJ,KAAK,eACL,KAAK,eACL,eAEI,yBAAA,CACA,sBAAA,CACA,iBAAA,CAGA,oBC3QJ,GAAG,OACD,UAAA,CACA,kBAGF,GAAG,MAAO,IACN,cAAA,CACA,mBAGJ,GAAG,MAAO,GACN,cAAA,CACA,mBAGJ,GAAG,mBACC,eAAA,CACA,eAAA,CACA;;;;EChBJ,UAAW,iBAGX,WAAY,YACZ,SAAU,cACV,WAAY,gBACZ,YAAa,cACb,UAAW,eACX,YAAa,iBACb,UAAW,gBACX,UAAW,WAGX,aAAc,uBACd,WAAY,qBACZ,aAAc,uBACd,cAAe,wBACf,YAAa,sBACb,cAAe,yBACf,YAAa,sBACb,YAAa,sBCtBb,GAAG,MACC,4BAAA,CHmDA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CD1DA,iBAAA,CANA,qBAAA,CACA,0BAAA,CACA,6BAAA,CIAA,iBAAA,CACA,kBAAA,CAkBA,UAAA,CACA,WAAA,CAEA,QAAA,CACA,aApBA,GARD,KAQE,UACG,qBAKJ,aAAA,GAdD,KAQE,UAIO,0BAIR,UAAW,IAhBZ,KAgBa,UACR,mBAKJ,aAAA,UANW,IAhBZ,KAgBa,UAIJ,0BAWZ,QAEI,cAAA,CAEA,YAAA,CACA,QAAA,CACA,qBAAA,CACA,gBAAA,CAEA,sBAWJ,QARmC,iBAG/B,QACI,iBAIR,GAAG,YHCC,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA0CA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OG1DJ,4BACI,GAAG,YAGC,mBAKR,GAAG,YACC,wBAAA,CJzDA,iBAAA,CI2DA,kBAAA,CACA,sBAMJ,GAAG,OAAO,OACN,aAAA,CACA,iBAGJ,GAAG,mBAEC,qBAAA,CH5DA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBG2CJ,GAAG,kBAKC,aJxEF,iBAAA,CI0EM,WAAA,CACA,gBAAA,CACA,SAAA,CACA,wBAAA,CACA,mBAXR,GAAG,kBAKC,YAQI,GACI,aAAA,CACA,qBAEA,GAjBT,kBAKC,YAQI,EAIK,OACG,aAAA,CACA,qBAWhB,QANmC,iBAE/B,GAAG,kBAAmB,IAAK,QACvB,cCtGR,aAAA,GALG,WAGK,yBAQR,GAAG,OACC,uBAAA,CJUA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBIbJ,QARmC,iBAE/B,GAAG,OJkCH,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,qBI3CJ,GAAG,cACC,UAAA,CACA,iCAQJ,GAAG,WAAY,IAAK,WAChB,WAAA,CACA,WAAA,CACA,SAAA,CACA,6BAGJ,GAAG,WAAY,IAAK,UAAW,KAC3B,QAAA,CACA,WAAA,CACA,SAAA,CACA,6BClCJ,YACI,qBAAA,CACA,cAAA,CACA,WAAA,CACA,gBAGJ,mBAGI,iBAAA,CACA,gBAGJ,kBAGI,aAGJ,uBAGI,oBAGJ,oBAGI,6BAAA,CACA,2BAGJ,WAAY,KAGR,SAAA,CACA,QAAA,CNnCF,gBOFF,gBACE,WAGF,oBAHE,WAOF,sBACE,cAGF,sBACE,WAGF,kBACE,cAGF,mBACE,aAAA,CACA,kBAGF,kBACE,WAGF,gBACE,WAGF,mBACE,aAAA,CACA,iBAGF,mBACE,cAGF,iBACE,WAGF,oBACE,UAAA,CACA,iBAGF,gBACE,WAIF,eC8BuB,WD3BvB,oBCoC4B,WDnC5B,qBCqC6B,WDpC7B,mBCsC2B,WDrC3B,eCsCuB,WDrCvB,qBCsC6B,WDrC7B,kBCsC0B,WDrC1B,iBCsCyB,WDrCzB,gBCuCwB,WDlCtB,aADY,KACX,YArCD,aAAA,CACA,iBAqCA,aAFY,KAEX,SA1CD,WA2CA,aAHY,KAGX,WA/CD,WAgDA,aAJY,KAIX,QCYoB,WDXrB,aALY,KAKX,aA1ED,WA6EA,aARY,KAQX,aA/BD,UAAA,CACA,iBA+BA,aATY,KASX,eAtED,cAuEA,aAVY,KAUX,eAnED,WAoEA,aAXY,KAWX,YA5DD,aAAA,CACA,kBA4DA,aAZY,KAYX,WAjED,cAkEA,aAbY,KAaX,aCYyB,WDX1B,aAdY,KAcX,SAhCD,WAiCA,aAfY,KAeX,cCY0B,WDX3B,aAhBY,KAgBX,YA/CD,cAgDA,aAjBY,KAiBX,YCYwB,WDXzB,aAlBY,KAkBX,QCYoB,WDXrB,aAnBY,KAmBX,cCY0B,WDX3B,aApBY,KAoBX,WCYuB,WDXxB,aArBY,KAqBX,UCYsB,WDXvB,aAtBY,KAsBX,SCaqB,WDZtB,aAvBY,KAuBX,UAlDD,WAoDA,aAzBY,KAyBX,QACC,sQAAA,CACA,yBAAA,CACA,4BE7GJ,GAAG,gBAEC,iBAAA,CRkDA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CQ9DA,UAIJ,GAAG,eAEC,WAAA,CAEA,UAAA,CAEA,aAAA,CTNA,iBAAA,CD2DF,kDAAA,CACQ,0CAAA,CUnDN,cAIJ,GAAG,kBACC,QAAA,CACA,SAAA,CR4BA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBQvCJ,GAAG,oBACC,WAAA,CACA,cAAA,CACA,iBAAA,CTtBA,kBS0BJ,GAAG,mBAAmB,OViCpB,qCAAA,CACQ,6BAAA,CU/BN,iCAGJ,GAAG,eACC,cAIJ,GAAG,aACC,SAAA,CACA,uBAAA,CR1BA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBQSJ,GAAG,YAKC,kBAEI,eAAA,YAPR,GAAG,YAUC,eAEI,OACI,aAAA,CACA,eAdZ,GAAG,YAUC,eAOI,KACI,aAAA,CACA,eAnBZ,GAAG,YAuBC,KAvBJ,GAAG,YAuBM,KACD,cAAA,CACA,YACA,GA1BL,YAuBC,IAGK,YAAD,GA1BL,YAuBM,IAGA,YACG,eAOZ,QR5BI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBQwBJ,QAPmC,iBAE/B,GAAG,aRlCH,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,qBQwBJ,GAAG,YAAa,KACZ,QAAA,CACA,SAAA,CACA,QAAA,CACA,uBAAA,CACA,WAAA,CACA,4BAAA,CTpFF,gBS0FF,GAAG,gBAEC,eAAA,CACA,YAAA,CRGA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,MAAA,CQLA,UAAW,kBAOf,GAAG,aACC,eAAA,CACA,UAAA,CAEA,sBAUJ,GAAG,eACC,gBAGJ,GAAG,cACC,gBAaJ,GAAG,kBAAkB,OACjB,UAGJ,UACI,cAKJ,GAAG,qBACC,qBAAA,CAGA,gBAOJ,KAAK,WACD,mBAAA,CACA,iBAAA,CACA,aAAA,CACA,UAAA,CAEA,uBAAA,CAEA,eAAA,CACA,eAGJ,KAAK,UAAU,OACX,gBAGJ,CAAC,SACG,mBAGJ,GAAG,qBACD,WAAA,CACA,gBAAA,CACA,UAHF,GAAG,oBAKD,GACI,aAAA,CACA,qBAEA,GATH,oBAKD,EAIK,OACG,aAAA,CACA,qBCxMV,eAEI,WAFJ,cAGI,IAAI,kBAHR,cAII,QAAQ,iBAJZ,cAKI,GAAG,0BALP,cAMI,OAAO,0BANX,cAOI,UAAU,0BAPd,cAYI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAZpE,cAaI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAbpE,cAcI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAdpE,cAeI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,cAf/D,cAgBI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,aAAA,CAAkB,kBAhBjF,cAiBI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,aAAA,CAAkB,kBAjBjF,cAoBI,GAAE,aAAc,kBApBpB,cAqBI,GAAE,aAAc,kBArBpB,cAsBI,GAAE,aAAc,kBAtBpB,cAuBI,GAAE,aAAc,eAvBpB,cAwBI,GAAE,aAAc,eAxBpB,cAyBI,GAAE,aAAc,eAzBpB,cA2BI,IAAI,eAAA,CAAiB,YAAA,CAAiB,eA3B1C,cA4BI,GAAG,IAAI,iBAAA,CAAmB,aA5B9B,cA6BI,GAAG,GAAG,IAAI,iBAAA,CAAmB,aA7BjC,cA8BI,IAAI,kBAAA,CAAoB,YAAA,CAAiB,eA9B7C,cA+BI,GAAG,IAAI,sBAAA,CAAwB,aA/BnC,cAgCI,GAAG,GAAG,IAAI,sBAAA,CAAwB,aAhCtC,cAiCI,GAAG,GAAG,GAAG,IAAI,sBAAA,CAAwB,aAjCzC,cAmCI,GAAG,GAAG,GAAG,GAAG,IAAI,kBAAA,CAAoB,aAnCxC,cAoCI,EAAE,IAAM,eApCZ,cAqCI,EAAE,IAAM,eArCZ,cAuCI,IACI,WAAA,CACA,uBAzCR,cA4CI,KAAK,eA5CT,cA8CI,KA9CJ,cA8CS,MACD,QAAA,CACA,qBAAA,CACA,UAAA,CACA,cAAA,CACA,UAnDR,cAsDI,YAAY,eAtDhB,cAwDI,OACI,gBAAA,CACA,iBAAA,CACA,sBAAA,CACA,yBA5DR,cA8DI,IA9DJ,cA8DQ,IA9DR,cA8DY,IACJ,sBAAA,CACA,wBAAA,CACA,eAjER,cAmEI,IAnEJ,cAmEQ,IACA,eAAA,CACA,qBAAA,CACA,YAtER,cAwEI,IAAI,iBAxER,cAyEI,EAAE,OAAS,eAzEf,cA2EI,GAAG,gBA3EP,cA4EI,EAAE,GAAK,eA5EX,cA8EI,KACI,aAAA,CACA,gBAAA,CACA,kBAjFR,cAmFI,EAAE,KAAO,eAnFb,cAqFI,KArFJ,cAqFS,KACD,cAAA,CACA,YACA,cAHJ,IAGK,YAAD,cAHC,IAGA,YACG,eCzFZ,GAAG,WVsBC,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBU3BJ,QAPmC,iBAE/B,GAAG,UAAW,IAAK,QACf,cAIR,GAAG,kBAEC,YAAA,CACA,WAAA,CACA,aAAA,CACA,iBAAA,CACA,2BAAA,CACA,UAAA,CXdA,qBAAA,CACA,0BAAA,CACA,8BWgBJ,CAAC,YAAY,MACX,oBAAA,CACA,cAAA,CACA,kBAIE,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cACJ,mBAIR,UAAU,SAAU,aAChB,aAGJ,UAAU,SAAU,gBAChB,gBAGJ,UAAU,WAAY,mBAClB,aAGJ,aACA,aACA,aACA,aACA,aACA,aACI,gBAAA,CACA,YCRsB,4CDW1B,aAAe,iBACf,aAAe,iBACf,aAAe,iBACf,aAAe,eACf,aACI,cAAA,CACA,kBAEJ,aACI,cAAA,CACA,kBE7DJ,gBACI,KADJ,gBACU,OACF,cAIR,aAgBI,uBAAA,CZJA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBY3BJ,YAmBI,iBACI,2BAAA,CACA,eAAA,Cb5BJ,qBAAA,CACA,0BAAA,CACA,6BAAA,CC+CA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA6DA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,MAAA,CAiFA,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBYpLA,YAAC,oBAAqB,QAAO,OACzB,QC0Ec,OD1Ed,CACA,YAAa,aAAb,CACA,aAAA,CACA,cAAA,CACA,OAAA,CACA,YAOR,aAEI,qBAAA,CACA,eAAA,Cb9CA,kBamDJ,gBAuBI,gBAAA,CACA,iBAAA,CACA,gBAAA,CAGA,WAAA,CACA,UAAA,CACA,cAAA,CACA,eAAA,CACA,kBAAA,CAtCA,qBAAA,CACA,eAAA,Cb9CA,iBAAA,CCaA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBYuBJ,eAwCI,YAEI,QAAA,CACA,eAAA,CZjFJ,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,mBAAA,CAwEA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OYtDJ,eAwCI,WAQI,mBACI,UAAA,CACA,WAAA,CACA,eAAA,CACA,kBApDZ,eAwCI,WAeI,kBACI,WAAA,CACA,eAAA,CACA,gBAKZ,gBAKI,kBAAA,CACA,gBAAA,CAGA,SAAA,CACA,aAAA,CACA,YAAA,CACA,gBAAA,CAjFA,qBAAA,CACA,eAAA,Cb9CA,iBAAA,CC2CA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBYwDJ,eAoBI,YAEI,QAAA,CACA,eAAA,CACA,gBAAA,CACA,cAAA,CZhGJ,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA0CA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OYSJ,eAoBI,WAUI,mBACI,UAAA,CACA,WAAA,CACA,gBAAA,CACA,kBAlCZ,eAoBI,WAiBI,kBACI,UAAA,CACA,gBAAA,CACA,gBAKZ,aAEI,WAAA,CACA,SAGJ,gBAEI,WAAA,CACA,gBAGJ,qBAEI,WAAA,CACA,SAGJ,iBAEI,cAAA,CACA,gBAHJ,gBAKI,eAEI,uBAAA,CACA,oBAAA,CACA,mBAAA,CACA,kBAAA,CACA,gBAIR,kBAGI,gBAGJ,YAAa,eAAe,GAAG,GAC3B,cAGJ,aZ7LI,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBY8KJ,YAII,MAAK,kBACD,cAAA,CACA,mBANR,YASI,eAEI,cAAA,CACA,iBAAA,CACA,eAAA,CACA,gBAAA,CACA,wBAfR,YAkBI,iBACI,gBAAA,CACA,eAAA,CACA,eAAA,CACA,wBAIR,aZzLI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBY0KJ,YAKI,eAEI,kBAAA,CACA,iBAAA,CACA,2BATR,YAYI,iBAEI,eAAA,CACA,iBAAA,CACA,wBAKR,Yb/PI,qBAAA,CACA,0BAAA,CACA,6BAAA,CCiNA,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBY6CJ,kBZpNI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CDhEA,qBAAA,CACA,0BAAA,CACA,6BAAA,CawQA,gBALJ,iBAOI,OACI,YAAA,CACA","file":"ipython.min.css"}
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/css/nikola_ipython.css b/nikola/data/themes/base/assets/css/nikola_ipython.css index 5ae5189..aba37da 100644 --- a/nikola/data/themes/base/assets/css/nikola_ipython.css +++ b/nikola/data/themes/base/assets/css/nikola_ipython.css @@ -40,77 +40,17 @@ div.text_cell_render { .rendered_html pre, .rendered_html code { background-color: #DDDDDD; + margin: 1em 0em; + font-size: 14px; +} + +.rendered_html pre { padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; - margin: 1em 0em; - font-size: 14px; } .page-content > .content p { margin: 0 0 0px; } - -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #408080; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ -.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #7D9029 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.highlight .no { color: #880000 } /* Name.Constant */ -.highlight .nd { color: #AA22FF } /* Name.Decorator */ -.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0000FF } /* Name.Function */ -.highlight .nl { color: #A0A000 } /* Name.Label */ -.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mf { color: #666666 } /* Literal.Number.Float */ -.highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/nikola/data/themes/base/assets/css/nikola_rst.css b/nikola/data/themes/base/assets/css/nikola_rst.css new file mode 100644 index 0000000..71a0f84 --- /dev/null +++ b/nikola/data/themes/base/assets/css/nikola_rst.css @@ -0,0 +1,79 @@ +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning, div.sidebar, +div.system-message { +/* stolen from Boostrap 4 (.card) */ + margin-bottom: 2rem; + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + color: #212529; + background-clip: border-box; + border: 1px solid rgba(0,0,0,.125); + border-radius: .25rem; + padding: 0; +} + +div.attention, div.caution, div.danger, div.error, div.warning { + /* stolen from Boostrap 3 (.border-danger) */ + border-color: #dc3545!important; +} + +div.admonition p, div.hint p, +div.important p, div.note p, +div.tip p, div.sidebar p, +div.attention p, div.caution p, +div.danger p, div.error p, +div.warning p, div.system-message p { + padding-left: 1rem; + padding-right: 1rem; +} + +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, 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, div.system-message p.system-message-title { +/* stolen from Boostrap 4 (.card .card-header) */ + font-weight: 400; + font-size: 1.25rem; + padding: .75rem 1.25rem; + margin-bottom: 1rem; + background-color: rgba(0,0,0,.03); + border-bottom: 1px solid rgba(0,0,0,.125); +} + +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, div.system-message p.system-message-title { + /* stolen from Boostrap 4 (.card .card-header .bg-danger) */ + background-color: #dc3545; + color: white; +} + +div.sidebar { + margin-right: 0; +} + +/* Improved margin overrides */ +div.topic, +pre.literal-block, +pre.doctest-block, +pre.math, +pre.code, +div.code { + margin-left: 1rem; + margin-right: 1rem; +} + +div.code { + margin-bottom: 1rem; +} diff --git a/nikola/data/themes/base/assets/css/rst.css b/nikola/data/themes/base/assets/css/rst.css index a1efa1a..03424a8 100644 --- a/nikola/data/themes/base/assets/css/rst.css +++ b/nikola/data/themes/base/assets/css/rst.css @@ -1,330 +1,2 @@ -/* -:Author: David Goodger (goodger@python.org) -: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. - -See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to -customize this style sheet. -*/ - -/* used to remove borders from tables and images */ -.borderless, table.borderless td, table.borderless th { - border: 0 } - -table.borderless td, table.borderless th { - /* Override padding for "table.docutils td" with "! important". - The right padding separates the table cells. */ - padding: 0 0.5em 0 0 ! important } - -.first { - /* Override more specific margin styles with "! important". */ - margin-top: 0 ! important } - -.last, .with-subtitle { - margin-bottom: 0 ! important } - -.hidden { - display: none } - -a.toc-backref { - text-decoration: none ; - color: black } - -blockquote.epigraph { - margin: 2em 5em ; } - -object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { - overflow: hidden; -} - -div.abstract { - margin: 2em 5em } - -div.abstract p.topic-title { - font-weight: bold ; - text-align: center } - -div.admonition, div.attention, div.caution, div.danger, div.error, -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, 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 { - /* 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. -div.compound .compound-first, div.compound .compound-middle { - margin-bottom: 0.5em } - -div.compound .compound-last, div.compound .compound-middle { - margin-top: 0.5em } -*/ - -div.dedication { - margin: 2em 5em ; - text-align: center ; - font-style: italic } - -div.dedication p.topic-title { - font-weight: bold ; - font-style: normal } - -div.figure { - margin-left: 2em ; - margin-right: 2em } - -div.footer, div.header { - clear: both; - font-size: smaller } - -div.line-block { - display: block ; - margin-top: 1em ; - margin-bottom: 1em } - -div.line-block div.line-block { - margin-top: 0 ; - margin-bottom: 0 ; - margin-left: 1.5em } - - -html[dir="rtl"] div.line-block div.line-block { - margin-top: 0 ; - margin-bottom: 0 ; - margin-right: 1.5em ; - margin-left: 0 ; -} - -div.sidebar { - margin-left: 2em; - min-height: 20px; - width: 40% ; - float: right ; - clear: right } - -div.sidebar p.rubric { - font-size: medium } - -div.system-messages { - margin: 5em } - -div.system-messages h1 { - color: #a94442 } - -div.system-message { - border: 1px solid #ebccd1; - padding: 1em } - -div.system-message p.system-message-title { - color: #a94442 ; - font-weight: bold } - -div.topic { - margin: 2em } - -img.align-left, .figure.align-left, object.align-left { - clear: left ; - float: left ; - margin-right: 1em } - -img.align-right, .figure.align-right, object.align-right { - clear: right ; - float: right ; - margin-left: 1em } - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left } - -.align-center { - clear: both ; - text-align: center } - -.align-right { - text-align: right } - -/* reset inner alignment in figures */ -.figure.align-right { - text-align: inherit } - -/* div.align-center * { */ -/* text-align: left } */ - -ol.simple, ul.simple { - margin-bottom: 1em } - -ol.arabic { - list-style: decimal } - -ol.loweralpha { - list-style: lower-alpha } - -ol.upperalpha { - list-style: upper-alpha } - -ol.lowerroman { - list-style: lower-roman } - -ol.upperroman { - list-style: upper-roman } - -p.attribution { - text-align: right ; - margin-left: 50% } - -p.caption { - font-style: italic } - -p.credits { - font-style: italic ; - font-size: smaller } - -p.label { - white-space: nowrap } - -p.rubric { - font-weight: bold ; - font-size: larger ; - color: maroon ; - text-align: center } - -p.sidebar-subtitle { - font-weight: bold } - -p.topic-title { - font-weight: bold } - -pre.address { - margin-bottom: 0 ; - margin-top: 0 ; - font: inherit } - -pre.code .ln { color: grey; } /* line numbers */ -/* -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-style: italic } - -span.classifier-delimiter { - font-weight: bold } - -span.option { - white-space: nowrap } - -span.pre { - white-space: pre } - -span.problematic { - color: red } - -span.section-subtitle { - /* font-size relative to parent (h1..h6 element) */ - font-size: 80% } - -table.citation { - border-left: solid 1px gray; - margin-left: 1px } - -table.docinfo { - margin: 2em 4em } - -table.docutils { - margin-top: 0.5em ; - margin-bottom: 0.5em } - -table.footnote { - border-left: solid 1px black; - margin-left: 1px } - -table.docutils td, table.docutils th, -table.docinfo td, table.docinfo th { - padding-left: 0.5em ; - padding-right: 0.5em ; - vertical-align: top } - -table.docutils th.field-name, table.docinfo th.docinfo-name { - font-weight: bold ; - text-align: left ; - 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% } - -ul.auto-toc { - list-style-type: none } - -a.footnote-reference { - line-height: 0px; -} +@import url("rst_base.css"); +@import url("nikola_rst.css"); diff --git a/nikola/data/themes/base/assets/css/rst_base.css b/nikola/data/themes/base/assets/css/rst_base.css new file mode 100644 index 0000000..429f7b5 --- /dev/null +++ b/nikola/data/themes/base/assets/css/rst_base.css @@ -0,0 +1,474 @@ +/* Minimal style sheet for the HTML output of Docutils. */ +/* */ +/* :Author: Günter Milde, based on html4css1.css by David Goodger */ +/* :Id: $Id: minimal.css 7952 2016-07-26 18:15:59Z milde $ */ +/* :Copyright: © 2015 Günter Milde. */ +/* :License: Released under the terms of the `2-Clause BSD license`_, */ +/* in short: */ +/* */ +/* Copying and distribution of this file, with or without modification, */ +/* are permitted in any medium without royalty provided the copyright */ +/* notice and this notice are preserved. */ +/* */ +/* This file is offered as-is, without any warranty. */ +/* */ +/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */ + +/* This CSS2.1_ stylesheet defines rules for Docutils elements without */ +/* HTML equivalent. It is required to make the document semantic visible. */ +/* */ +/* .. _CSS2.1: http://www.w3.org/TR/CSS2 */ +/* .. _validates: http://jigsaw.w3.org/css-validator/validator$link */ + +/* alignment of text and inline objects inside block objects*/ +.align-left { text-align: left; } +.align-right { text-align: right; } +.align-center { clear: both; text-align: center; } +.align-top { vertical-align: top; } +.align-middle { vertical-align: middle; } +.align-bottom { vertical-align: bottom; } + +/* titles */ +h1.title, p.subtitle { + text-align: center; +} +p.admonition-title, +p.topic-title, +p.sidebar-title, +p.rubric, +p.system-message-title { + font-weight: bold; +} +h1 + p.subtitle, +h1 + p.section-subtitle { + font-size: 1.6em; +} +h2 + p.section-subtitle { font-size: 1.28em; } +p.subtitle, +p.section-subtitle, +p.sidebar-subtitle { + font-weight: bold; + margin-top: -0.5em; +} +p.sidebar-title, +p.rubric { + font-size: larger; +} +p.rubric { color: maroon; } +a.toc-backref { + color: black; + text-decoration: none; } + +/* Warnings, Errors */ +div.caution p.admonition-title, +div.attention p.admonition-title, +div.danger p.admonition-title, +div.error p.admonition-title, +div.warning p.admonition-title, +div.system-messages h1, +div.error, +span.problematic, +p.system-message-title { + color: red; +} + +/* inline literals */ +span.docutils.literal { + font-family: monospace; + white-space: pre-wrap; +} +/* do not wraph at hyphens and similar: */ +.literal > span.pre { white-space: nowrap; } + +/* Lists */ + +/* compact and simple lists: no margin between items */ +.simple li, .compact li, +.simple ul, .compact ul, +.simple ol, .compact ol, +.simple > li p, .compact > li p, +dl.simple > dd, dl.compact > dd { + margin-top: 0; + margin-bottom: 0; +} + +/* Table of Contents */ +/*div.topic.contents { margin: 0; }*/ +ul.auto-toc { + list-style-type: none; + padding-left: 1.5em; } + +/* Enumerated Lists */ +ol.arabic { list-style: decimal } +ol.loweralpha { list-style: lower-alpha } +ol.upperalpha { list-style: upper-alpha } +ol.lowerroman { list-style: lower-roman } +ol.upperroman { list-style: upper-roman } + +dt span.classifier { font-style: italic } +dt span.classifier:before { + font-style: normal; + margin: 0.5em; + content: ":"; +} + +/* Field Lists and drivatives */ +/* bold field name, content starts on the same line */ +dl.field-list > dt, +dl.option-list > dt, +dl.docinfo > dt, +dl.footnote > dt, +dl.citation > dt { + font-weight: bold; + clear: left; + float: left; + margin: 0; + padding: 0; + padding-right: 0.5em; +} +/* Offset for field content (corresponds to the --field-name-limit option) */ +dl.field-list > dd, +dl.option-list > dd, +dl.docinfo > dd { + margin-left: 9em; /* ca. 14 chars in the test examples */ +} +/* start field-body on a new line after long field names */ +dl.field-list > dd > *:first-child, +dl.option-list > dd > *:first-child +{ + display: inline-block; + width: 100%; + margin: 0; +} +/* field names followed by a colon */ +dl.field-list > dt:after, +dl.docinfo > dt:after { + content: ":"; +} + +/* Bibliographic Fields (docinfo) */ +pre.address { font: inherit; } +dd.authors > p { margin: 0; } + +/* Option Lists */ +dl.option-list { margin-left: 40px; } +dl.option-list > dt { font-weight: normal; } +span.option { white-space: nowrap; } + +/* Footnotes and Citations */ +dl.footnote.superscript > dd {margin-left: 1em; } +dl.footnote.brackets > dd {margin-left: 2em; } +dl > dt.label { font-weight: normal; } +a.footnote-reference.brackets:before, +dt.label > span.brackets:before { content: "["; } +a.footnote-reference.brackets:after, +dt.label > span.brackets:after { content: "]"; } +a.footnote-reference.superscript, +dl.footnote.superscript > dt.label { + vertical-align: super; + font-size: smaller; +} +dt.label > span.fn-backref { margin-left: 0.2em; } +dt.label > span.fn-backref > a { font-style: italic; } + +/* Line Blocks */ +div.line-block { display: block; } +div.line-block div.line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 40px; +} + +/* Figures, Images, and Tables */ +.figure.align-left, +img.align-left, +object.align-left, +table.align-left { + margin-right: auto; +} +.figure.align-center, +img.align-center, +object.align-center { + margin-left: auto; + margin-right: auto; + display: block; +} +table.align-center { + margin-left: auto; + margin-right: auto; +} +.figure.align-right, +img.align-right, +object.align-right, +table.align-right { + margin-left: auto; +} +/* reset inner alignment in figures and tables */ +div.align-left, div.align-center, div.align-right, +table.align-left, table.align-center, table.align-right +{ text-align: inherit } + +/* Admonitions and System Messages */ +div.admonition, +div.system-message, +div.sidebar{ + margin: 40px; + border: medium outset; + padding-right: 1em; + padding-left: 1em; +} + +/* Sidebar */ +div.sidebar { + width: 30%; + max-width: 26em; + float: right; + clear: right; +} + +/* Text Blocks */ +div.topic, +pre.literal-block, +pre.doctest-block, +pre.math, +pre.code { + margin-right: 40px; + margin-left: 40px; +} +pre.code .ln { color: gray; } /* line numbers */ + +/* Tables */ +table.docutils { border-collapse: collapse; } +table.docutils > td, table.docutils > th { + border-style: solid; + border-color: silver; + padding: 0 1ex; + border-width: thin; +} +table.docutils > td > p:first-child, table.docutils > th > p:first-child { margin-top: 0; } +table.docutils > td > p, table.docutils > th > p { margin-bottom: 0; } + +table.docutils > caption { + text-align: left; + margin-bottom: 0.25em +} + +table.borderless td, table.borderless th { + border: 0; + padding: 0; + padding-right: 0.5em /* separate table cells */ +} + +/* CSS31_ style sheet for the output of Docutils HTML writers. */ +/* Rules for easy reading and pre-defined style variants. */ +/* */ +/* :Author: Günter Milde, based on html4css1.css by David Goodger */ +/* :Id: $Id: plain.css 7952 2016-07-26 18:15:59Z milde $ */ +/* :Copyright: © 2015 Günter Milde. */ +/* :License: Released under the terms of the `2-Clause BSD license`_, */ +/* in short: */ +/* */ +/* Copying and distribution of this file, with or without modification, */ +/* are permitted in any medium without royalty provided the copyright */ +/* notice and this notice are preserved. */ +/* */ +/* This file is offered as-is, without any warranty. */ +/* */ +/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */ +/* .. _CSS3: http://www.w3.org/TR/CSS3 */ + + +/* Document Structure */ +/* ****************** */ + +/* Sections */ + +/* Transitions */ + +hr.docutils { + width: 80%; + margin-top: 1em; + margin-bottom: 1em; + clear: both; +} + +/* Paragraphs */ +/* ========== */ + +/* vertical space (parskip) */ +/*p, ol, ul, dl,*/ +/*div.line-block,*/ +/*table{*/ + /*margin-top: 0.5em;*/ + /*margin-bottom: 0.5em;*/ +/*}*/ +/*h1, h2, h3, h4, h5, h6, */ +dl > dd { + margin-bottom: 0.5em; +} + +/* Lists */ +/* ========== */ + +/* Definition Lists */ + +dl > dd p:first-child { margin-top: 0; } +/* :last-child is not part of CSS 2.1 (introduced in CSS 3) */ +/* dl > dd p:last-child { margin-bottom: 0; } */ + +/* lists nested in definition lists */ +/* :only-child is not part of CSS 2.1 (introduced in CSS 3) */ +dd > ul:only-child, dd > ol:only-child { padding-left: 1em; } + +/* Description Lists */ +/* styled like in most dictionaries, encyclopedias etc. */ +dl.description > dt { + font-weight: bold; + clear: left; + float: left; + margin: 0; + padding: 0; + padding-right: 0.5em; +} + +/* Field Lists */ + +/* example for custom field-name width */ +dl.field-list.narrow > dd { + margin-left: 5em; +} +/* run-in: start field-body on same line after long field names */ +dl.field-list.run-in > dd p { + display: block; +} + +/* Bibliographic Fields */ + +/* generally, bibliographic fields use special definition list dl.docinfo */ +/* but dedication and abstract are placed into "topic" divs */ +div.abstract p.topic-title { + text-align: center; +} +div.dedication { + margin: 2em 5em; + text-align: center; + font-style: italic; +} +div.dedication p.topic-title { + font-style: normal; +} + +/* Citations */ +dl.citation dt.label { + font-weight: bold; +} +span.fn-backref { + font-weight: normal; +} + +/* Text Blocks */ +/* ============ */ + +/* Literal Blocks */ +pre.literal-block, pre.doctest-block, +pre.math, pre.code { + margin-left: 1.5em; + margin-right: 1.5em +} + +/* Block Quotes */ + +blockquote, +div.topic { + margin-left: 1.5em; + margin-right: 1.5em +} +blockquote > table, +div.topic > table { + margin-top: 0; + margin-bottom: 0; +} +blockquote p.attribution, +div.topic p.attribution { + text-align: right; + margin-left: 20%; +} + +/* Tables */ +/* ====== */ + +/* th { vertical-align: bottom; } */ + +table tr { text-align: left; } + +/* "booktabs" style (no vertical lines) */ +table.booktabs { + border: 0; + border-top: 2px solid; + border-bottom: 2px solid; + border-collapse: collapse; +} +table.booktabs * { + border: 0; +} +table.booktabs th { + border-bottom: thin solid; +} + +/* numbered tables (counter defined in div.document) */ +table.numbered > caption:before { + counter-increment: table; + content: "Table " counter(table) ": "; + font-weight: bold; +} + +/* Explicit Markup Blocks */ +/* ====================== */ + +/* Footnotes and Citations */ +/* ----------------------- */ + +/* line on the left */ +dl.footnote { + padding-left: 1ex; + border-left: solid; + border-left-width: thin; +} + +/* Directives */ +/* ---------- */ + +/* Body Elements */ +/* ~~~~~~~~~~~~~ */ + +/* Images and Figures */ + +/* let content flow to the side of aligned images and figures */ +.figure.align-left, +img.align-left, +object.align-left { + display: block; + clear: left; + float: left; + margin-right: 1em +} +.figure.align-right, +img.align-right, +object.align-right { + display: block; + clear: right; + float: right; + margin-left: 1em +} + +/* Sidebar */ + +/* Move into the margin. In a layout with fixed margins, */ +/* it can be moved into the margin completely. */ +div.sidebar { + width: 30%; + max-width: 26em; + margin-left: 1em; + margin-right: -5.5%; + background-color: #ffffee ; +} diff --git a/nikola/data/themes/base/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css index 4842a3f..076351f 100644 --- a/nikola/data/themes/base/assets/css/theme.css +++ b/nikola/data/themes/base/assets/css/theme.css @@ -1,7 +1,7 @@ @charset "UTF-8"; /* - Copyright © 2014-2016 Daniel Aleksandersen and others. + Copyright © 2014-2020 Daniel Aleksandersen and others. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -67,9 +67,9 @@ body { margin-right: 1.5em; } -#menu ul li:dir(rtl), -#toptranslations ul li:dir(rtl), -#toptranslations h2:dir(rtl) { +html[dir="rtl"] #menu ul li, +html[dir="rtl"] #toptranslations ul li, +html[dir="rtl"] #toptranslations h2 { margin-left: 1.5em; margin-right: 0; } @@ -79,12 +79,12 @@ body { float: right; } -#toptranslations:dir(rtl) { +html[dir="rtl"] #toptranslations { text-align: left; float: left; } -.posttranslations h3 { +.posttranslations h3, .translationslist h3 { display: inline; font-size: 1em; } @@ -93,7 +93,7 @@ body { font-size: 2em; } -.posttranslations h3:last-child { +.posttranslations h3:last-child, .translationslist h3:last-child { display: none; } @@ -120,35 +120,50 @@ body { } .metadata p:before, .postpromonav .tags li:before, -.postlist .listdate:after { +.postlist .listdate:after, +.translationslist p:before { content: " — "; } .postlist li { margin-bottom: .33em; } +.byline a:not(:last-child):after { + content: ","; +} /* Post and archive pagers */ .postindexpager .pager .next:before { content: "↓ "; } -.postindexpager .pager .previous:before { +.postindexpager .pager .previous:before, +.archivenav .pager .up:before { content: "↑ "; } -.postpromonav .pager .next:after { +.postpromonav .pager .next:after, +.archivenav .pager .next:after { content: " →"; } -.postpromonav .pager .previous:dir(rtl):after { +html[dir="rtl"] .postpromonav .pager .previous:after, +html[dir="rtl"] .archivenav .pager .previous:after { content: " →"; } -.postpromonav .pager .previous:before { +.postpromonav .pager .previous:before, +.archivenav .pager .previous:before { content: "← "; } -.postpromonav .pager .next:dir(rtl):before { +html[dir="rtl"] .postpromonav .pager .next:before, +html[dir="rtl"] .archivenav .pager .next:before { content: "← "; } - +html[dir="rtl"] .postpromonav .pager .next:after, +html[dir="rtl"] .archivenav .pager .next:after, +html[dir="rtl"] .postpromonav .pager .previous:before, +html[dir="rtl"] .archivenav .pager .previous:before { + content: ""; +} .metadata p:first-of-type:before, -.postpromonav .tags li:first-of-type:before { +.postpromonav .tags li:first-of-type:before, +.translationslist p:first-of-type:before { content: ""; } .postpromonav .pager { @@ -156,24 +171,40 @@ body { height: 1em; } .postpromonav .tags li, -.postpromonav .pager li { +.postpromonav .pager li, +.archivenav .pager li { display: inline-block; } -.postpromonav .pager .next { +.archivenav .pager { + text-align: center +} +.postpromonav .pager .next, +.archivenav .pager .next { float: right; } -.postpromonav .pager .next:dir(rtl) { +html[dir="rtl"] .postpromonav .pager .next, +html[dir="rtl"] .archivenav .pager .next { float: left; } -.postpromonav .pager .previous { +.postpromonav .pager .previous, +.archivenav .pager .previous { float: left; } -.postpromonav .pager .previous:dir(rtl) { +html[dir="rtl"] .postpromonav .pager .previous, +html[dir="rtl"] .archivenav .pager .previous { float: right; } -.metadata p { +.archivenav .pager .disabled, +.archivenav .pager .disabled a, +.archivenav .pager .disabled:link { + color: #888; + cursor: not-allowed; +} + +.metadata p, +.translationslist p { display: inline; } @@ -254,10 +285,6 @@ img { margin-right: 0; } -.codetable .linenos { - padding-right: 10px; -} - .sr-only { position: absolute; width: 1px; @@ -280,7 +307,7 @@ img { } pre.code, code { - white-space: pre; + white-space: pre-wrap; word-wrap: normal; overflow: auto; } diff --git a/nikola/data/themes/base/assets/js/baguetteBox.min.js b/nikola/data/themes/base/assets/js/baguetteBox.min.js new file mode 120000 index 0000000..dda9b55 --- /dev/null +++ b/nikola/data/themes/base/assets/js/baguetteBox.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/js/fancydates.js b/nikola/data/themes/base/assets/js/fancydates.js index d13b11b..dc7906d 100644 --- a/nikola/data/themes/base/assets/js/fancydates.js +++ b/nikola/data/themes/base/assets/js/fancydates.js @@ -1,19 +1,21 @@ -function fancydates(fanciness, date_format) { - if (fanciness == 0) { +function fancydates(fanciness, luxonDateFormat) { + if (fanciness === 0) { return; } - dates = $('time.published.dt-published'); + var dates = document.querySelectorAll('.dt-published, .dt-updated, .listdate'); - i = 0; - l = dates.length; + var l = dates.length; - for (i = 0; i < l; i++) { - d = moment(dates[i].attributes.datetime.value); - if (fanciness == 1) { - o = d.local().format(date_format); + for (var i = 0; i < l; i++) { + var d = luxon.DateTime.fromISO(dates[i].attributes.datetime.value); + var o; + if (fanciness === 1 && luxonDateFormat.preset) { + o = d.toLocal().toLocaleString(luxon.DateTime[luxonDateFormat.format]); + } else if (fanciness === 1) { + o = d.toLocal().toFormat(luxonDateFormat.format); } else { - o = d.fromNow(); + o = d.toRelative(); } dates[i].innerHTML = o; } diff --git a/nikola/data/themes/base/assets/js/fancydates.min.js b/nikola/data/themes/base/assets/js/fancydates.min.js new file mode 100644 index 0000000..bb0b07b --- /dev/null +++ b/nikola/data/themes/base/assets/js/fancydates.min.js @@ -0,0 +1 @@ +function fancydates(t,e){if(0!==t)for(var a=document.querySelectorAll(".dt-published, .dt-updated, .listdate"),o=a.length,l=0;l<o;l++){var r,i=luxon.DateTime.fromISO(a[l].attributes.datetime.value);r=1===t&&e.preset?i.toLocal().toLocaleString(luxon.DateTime[e.format]):1===t?i.toLocal().toFormat(e.format):i.toRelative(),a[l].innerHTML=r}} diff --git a/nikola/data/themes/base/assets/js/gallery.js b/nikola/data/themes/base/assets/js/gallery.js new file mode 100644 index 0000000..af29f47 --- /dev/null +++ b/nikola/data/themes/base/assets/js/gallery.js @@ -0,0 +1,32 @@ +function renderGallery(jsonContent, thumbnailSize) { + var container = document.getElementById("gallery_container"); + container.innerHTML = ''; + var layoutGeometry = require('justified-layout')(jsonContent, { + "containerWidth": container.offsetWidth, + "targetRowHeight": thumbnailSize * 0.6, + "boxSpacing": 5}); + container.style.height = layoutGeometry.containerHeight + 'px'; + var boxes = layoutGeometry.boxes; + for (var i = 0; i < boxes.length; i++) { + var img = document.createElement("img"); + img.setAttribute('src', jsonContent[i].url_thumb); + img.setAttribute('alt', jsonContent[i].title); + img.style.width = boxes[i].width + 'px'; + img.style.height = boxes[i].height + 'px'; + link = document.createElement("a"); + link.setAttribute('href', jsonContent[i].url); + link.setAttribute('class', 'image-reference'); + div = document.createElement("div"); + div.setAttribute('class', 'image-block'); + div.setAttribute('title', jsonContent[i].title); + div.setAttribute('data-toggle', "tooltip") + div.style.width = boxes[i].width + 'px'; + div.style.height = boxes[i].height + 'px'; + div.style.top = boxes[i].top + 'px'; + div.style.left = boxes[i].left + 'px'; + link.appendChild(img); + div.appendChild(link); + container.appendChild(div); + } +} + diff --git a/nikola/data/themes/base/assets/js/gallery.min.js b/nikola/data/themes/base/assets/js/gallery.min.js new file mode 100644 index 0000000..c434155 --- /dev/null +++ b/nikola/data/themes/base/assets/js/gallery.min.js @@ -0,0 +1 @@ +function renderGallery(t,e){var i=document.getElementById("gallery_container");i.innerHTML="";var l=require("justified-layout")(t,{containerWidth:i.offsetWidth,targetRowHeight:.6*e,boxSpacing:5});i.style.height=l.containerHeight+"px";for(var n=l.boxes,r=0;r<n.length;r++){var a=document.createElement("img");a.setAttribute("src",t[r].url_thumb),a.setAttribute("alt",t[r].title),a.style.width=n[r].width+"px",a.style.height=n[r].height+"px",link=document.createElement("a"),link.setAttribute("href",t[r].url),link.setAttribute("class","image-reference"),div=document.createElement("div"),div.setAttribute("class","image-block"),div.setAttribute("title",t[r].title),div.setAttribute("data-toggle","tooltip"),div.style.width=n[r].width+"px",div.style.height=n[r].height+"px",div.style.top=n[r].top+"px",div.style.left=n[r].left+"px",link.appendChild(a),div.appendChild(link),i.appendChild(div)}} diff --git a/nikola/data/themes/base/assets/js/html5.js b/nikola/data/themes/base/assets/js/html5.js index 448cebd..31340f0 100644..120000 --- a/nikola/data/themes/base/assets/js/html5.js +++ b/nikola/data/themes/base/assets/js/html5.js @@ -1,8 +1 @@ -/* - HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed -*/ -(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); -a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>"; -c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| -"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); -if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document); +../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js b/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js new file mode 120000 index 0000000..31340f0 --- /dev/null +++ b/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/js/justified-layout.min.js b/nikola/data/themes/base/assets/js/justified-layout.min.js new file mode 120000 index 0000000..d067ee6 --- /dev/null +++ b/nikola/data/themes/base/assets/js/justified-layout.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/justified-layout/dist/justified-layout.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/js/luxon.min.js b/nikola/data/themes/base/assets/js/luxon.min.js new file mode 120000 index 0000000..a8a639d --- /dev/null +++ b/nikola/data/themes/base/assets/js/luxon.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/luxon/build/global/luxon.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/assets/js/moment-with-locales.min.js b/nikola/data/themes/base/assets/js/moment-with-locales.min.js deleted file mode 120000 index 1caedc6..0000000 --- a/nikola/data/themes/base/assets/js/moment-with-locales.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/moment/min/moment-with-locales.min.js
\ No newline at end of file diff --git a/nikola/data/themes/base/base.theme b/nikola/data/themes/base/base.theme new file mode 100644 index 0000000..3dd0fd1 --- /dev/null +++ b/nikola/data/themes/base/base.theme @@ -0,0 +1,9 @@ +[Theme] +engine = mako +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT + +[Family] +family = base +jinja_version = base-jinja diff --git a/nikola/data/themes/base/bundles b/nikola/data/themes/base/bundles index 4760181..186e40e 100644 --- a/nikola/data/themes/base/bundles +++ b/nikola/data/themes/base/bundles @@ -1,2 +1,19 @@ -assets/css/all.css=rst.css,code.css,theme.css -assets/css/all-nocdn.css=rst.css,code.css,theme.css +; css bundles +assets/css/all.css= + rst_base.css, + nikola_rst.css, + code.css, + theme.css, +assets/css/all-nocdn.css= + rst_base.css, + nikola_rst.css, + code.css, + theme.css, + baguetteBox.min.css, + +; javascript bundles +assets/js/all.js= + fancydates.js, +assets/js/all-nocdn.js= + baguetteBox.min.js, + fancydates.js, diff --git a/nikola/data/themes/base/engine b/nikola/data/themes/base/engine deleted file mode 100644 index 2951cdd..0000000 --- a/nikola/data/themes/base/engine +++ /dev/null @@ -1 +0,0 @@ -mako diff --git a/nikola/data/themes/base/messages/messages_af.py b/nikola/data/themes/base/messages/messages_af.py new file mode 100644 index 0000000..650676b --- /dev/null +++ b/nikola/data/themes/base/messages/messages_af.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "%d min oor om te lees", + "(active)": "(aktief)", + "Also available in:": "Ook beskikbaar in:", + "Archive": "Argief", + "Atom feed": "Atom-voer", + "Authors": "Outeurs", + "Categories": "Kategorieë", + "Comments": "Opmerkings", + "LANGUAGE": "Afrikaans", + "Languages:": "Tale:", + "More posts about %s": "Meer plasings oor %s", + "Newer posts": "Jonger plasings", + "Next post": "Volgende plasing", + "Next": "Volgende", + "No posts found.": "Geen plasings gevind nie.", + "Nothing found.": "Niks gevind nie.", + "Older posts": "Ouer plasings", + "Original site": "Oorspronklike werf", + "Posted:": "Geplaas:", + "Posts about %s": "Plasings oor %s", + "Posts by %s": "Plasings deur %s", + "Posts for year %s": "Plasings vir %s", + "Posts for {month_day_year}": "Plasings vir {month_day_year}", + "Posts for {month_year}": "Plasings vir {month_year}", + "Previous post": "Vorige plasing", + "Previous": "Vorige", + "Publication date": "Publikasiedatum", + "RSS feed": "RSS-voer", + "Read in English": "Lees in Afrikaans", + "Read more": "Lees meer", + "Skip to main content": "Spring na die hoofinhoud", + "Source": "Bron", + "Subcategories:": "Subkategorieë:", + "Tags and Categories": "Etikette en kategorieë", + "Tags": "Etikette", + "Toggle navigation": "Wissel navigasie", + "Uncategorized": "Ongekategoriseerd", + "Up": "Op", + "Updates": "Bywerkings", + "Write your page here.": "Skryf die bladsy hier.", + "Write your post here.": "Skryf die plasing hier.", + "old posts, page %d": "ou plasings, bladsy %d", + "page %d": "bladsy %d", + "updated": "bygewerk", +} diff --git a/nikola/data/themes/base/messages/messages_ar.py b/nikola/data/themes/base/messages/messages_ar.py index 4990ad4..72c137b 100644 --- a/nikola/data/themes/base/messages/messages_ar.py +++ b/nikola/data/themes/base/messages/messages_ar.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "", - "(active)": "", + "%d min remaining to read": "%d دقائق متبقية للقراءة", + "(active)": "(نشط)", "Also available in:": "أيضا متوفر في:", "Archive": "الأرشيف", - "Authors": "", + "Atom feed": "روابط Atom", + "Authors": "المؤلفون", "Categories": "فئات", "Comments": "التّعليقات", "LANGUAGE": "العربيّة", - "Languages:": "اللغات", + "Languages:": "اللُغَات", "More posts about %s": "المزيد من المقالات حول %s", "Newer posts": "مقالات أحدث", "Next post": "المقالة التالية", + "Next": "التالي", "No posts found.": "لم يوجد مقالات.", - "Nothing found.": "لم يوجد شيء.", + "Nothing found.": "لا يوجد شيء.", "Older posts": "مقالات أقدم", "Original site": "الموقع الأصلي", "Posted:": "نشر:", "Posts about %s": "مقالات عن %s", - "Posts by %s": "", + "Posts by %s": "مقالات بواسطة %s", "Posts for year %s": "مقالات سنة %s", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "", + "Posts for {month_day_year}": "مقال لـ {شَهْر - يَوْم - سَنَة}", + "Posts for {month_year}": "مقالـ {شَهْر - سَنَة }", "Previous post": "المقالة السابقة", - "Publication date": "تاريخ النشر", - "RSS feed": "", + "Previous": "السابق", + "Publication date": "تاريخ النَشْر", + "RSS feed": "روابط التغذية ", "Read in English": "اقرأ بالعربية", "Read more": "قراءة المزيد", "Skip to main content": "انتقل إلى المحتوى الرئيسي", "Source": "المصدر", - "Subcategories:": "", + "Subcategories:": "فئات فرعية", "Tags and Categories": "تصنيفات و فئات", "Tags": "تصنيفات", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", - "Write your page here.": "", - "Write your post here.": "", + "Toggle navigation": "تَفْعيل وَضْع التَنَقُلْ", + "Uncategorized": "غير مصنف", + "Up": "أعلى", + "Updates": "التَحْدِيثَات", + "Write your page here.": "اكْتُب صَفْحَتك هُنَا ", + "Write your post here.": "اكْتُب مَقَالَك هُنَا", "old posts, page %d": "مقالات قديمة, صفحة %d", "page %d": "صفحة %d", + "updated": "مُحدَّثة", } diff --git a/nikola/data/themes/base/messages/messages_az.py b/nikola/data/themes/base/messages/messages_az.py index 11e45ba..b7913f2 100644 --- a/nikola/data/themes/base/messages/messages_az.py +++ b/nikola/data/themes/base/messages/messages_az.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d dəqiqəlik oxuma", "(active)": "(aktiv)", "Also available in:": "Həmçinin mövcuddur:", "Archive": "Arxiv", + "Atom feed": "Atom feed", "Authors": "Müəlliflər", "Categories": "Kateqoriyalar", "Comments": "Şərhlər", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "%s ilə bağlı digər yazılar", "Newer posts": "Yeni yazılar", "Next post": "Növbəti yazı", + "Next": "Növbəti", "No posts found.": "Heç bir yazı tapılmadı", "Nothing found.": "Heç nə tapılmadı", "Older posts": "Köhnə yazılar", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "%s ilə bağlı yazılar", "Posts by %s": "%s tərəfindən yazılmış yazılar", "Posts for year %s": "%s ilindəki yazılar", - "Posts for {month} {day}, {year}": "{month} {day}, {year} üçün yazılar", - "Posts for {month} {year}": "{month} {year} üçün yazılar", + "Posts for {month_day_year}": "{month_day_year} üçün yazılar", + "Posts for {month_year}": "{month_year} üçün yazılar", "Previous post": "Əvvəlki yazı", + "Previous": "Öncəki", "Publication date": "Buraxılış tarixi", "RSS feed": "RSS", "Read in English": "Azərbaycan dilində oxu", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Subkateqoriyalar", "Tags and Categories": "Teqlər və Kateqoriyalar", "Tags": "Teqlər", - "Toggle navigation": "", + "Toggle navigation": "Naviqasiya keçidi", "Uncategorized": "Kateqoriyasız", + "Up": "Yuxarı", "Updates": "Yenilənmələr", "Write your page here.": "Öz səhifəni bura yaz", "Write your post here.": "Öz məqaləni bura yaz", "old posts, page %d": "köhnə yazılar, səhifə %s", "page %d": "səhifə %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py index 2e4d400..3344e04 100644 --- a/nikola/data/themes/base/messages/messages_bg.py +++ b/nikola/data/themes/base/messages/messages_bg.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d минути до прочитане", "(active)": "(активно)", "Also available in:": "Достъпно също на:", "Archive": "Архив", + "Atom feed": "", "Authors": "Автори", "Categories": "Категории", "Comments": "Коментари", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Още публикации относно %s", "Newer posts": "Нови публикации", "Next post": "Следваща публикация", + "Next": "", "No posts found.": "Не са намерени публикации.", "Nothing found.": "Нищо не е намерено.", "Older posts": "Стари публикации", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Публикации относно %s", "Posts by %s": "Публикации от %s", "Posts for year %s": "Публикации за %s година", - "Posts for {month} {day}, {year}": "Публикации от {day} {month} {year}", - "Posts for {month} {year}": "Публикации за {month} {year}", + "Posts for {month_day_year}": "Публикации от {month_day_year}", + "Posts for {month_year}": "Публикации за {month_year}", "Previous post": "Предишна публикация", + "Previous": "", "Publication date": "Дата на публикуване", "RSS feed": "RSS поток", "Read in English": "Прочетете на български", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Тагове", "Toggle navigation": "", "Uncategorized": "Без категория", + "Up": "", "Updates": "Обновления", "Write your page here.": "Напиши тук текста на твоята страница.", "Write your post here.": "Напиши тук текста на твоята публикация.", "old posts, page %d": "стари публикации, страница %d", "page %d": "страница %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_fil.py b/nikola/data/themes/base/messages/messages_br.py index 6107c54..316e4ec 100644 --- a/nikola/data/themes/base/messages/messages_fil.py +++ b/nikola/data/themes/base/messages/messages_br.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "", "(active)": "", "Also available in:": "", "Archive": "", + "Atom feed": "", "Authors": "", "Categories": "", "Comments": "", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "", "Newer posts": "", "Next post": "", + "Next": "", "No posts found.": "", "Nothing found.": "", "Older posts": "", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "", "Posts by %s": "", "Posts for year %s": "", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "", + "Posts for {month_day_year}": "", + "Posts for {month_year}": "", "Previous post": "", + "Previous": "", "Publication date": "", "RSS feed": "", "Read in English": "", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "", "Write your post here.": "", "old posts, page %d": "", "page %d": "", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_bs.py b/nikola/data/themes/base/messages/messages_bs.py index 9d7ac6f..2d537e6 100644 --- a/nikola/data/themes/base/messages/messages_bs.py +++ b/nikola/data/themes/base/messages/messages_bs.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minuta preostalo za čitanje", "(active)": "(aktivno)", "Also available in:": "Takođe dostupan u:", "Archive": "Arhiva", + "Atom feed": "", "Authors": "Autori", "Categories": "Kategorije", "Comments": "Komentari", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Više članaka o %s", "Newer posts": "Novije objave", "Next post": "Naredni članak", + "Next": "", "No posts found.": "Nema članaka.", "Nothing found.": "Ništa nije pronađeno.", "Older posts": "Starije objave", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Objave o %s", "Posts by %s": "Objave prema %s", "Posts for year %s": "Objave u godini %s", - "Posts for {month} {day}, {year}": "Objave za {day}.{month}.{year}", - "Posts for {month} {year}": "Objave za {month} {year}", + "Posts for {month_day_year}": "Objave za {month_day_year}", + "Posts for {month_year}": "Objave za {month_year}", "Previous post": "Prethodni članak", + "Previous": "", "Publication date": "Datum objavljivanja", "RSS feed": "RSS feed", "Read in English": "Pročitaj na bosanskom", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Oznake", "Toggle navigation": "", "Uncategorized": "Bez kategorije", + "Up": "", "Updates": "Ažuriranja", "Write your page here.": "Vašu stranicu napišite ovdje.", "Write your post here.": "Vaš članak napišite ovdje.", "old posts, page %d": "stare objave, strana %d", "page %d": "strana %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py index f3aebdf..0143a85 100644 --- a/nikola/data/themes/base/messages/messages_ca.py +++ b/nikola/data/themes/base/messages/messages_ca.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "% min recordar per a llegir", - "(active)": "", - "Also available in:": "També disponibles en:", + "%d min remaining to read": "% min restants per a llegir", + "(active)": "(actiu)", + "Also available in:": "També disponible en:", "Archive": "Arxiu", - "Authors": "", + "Atom feed": "Canal Atom", + "Authors": "Autors", "Categories": "Categories", "Comments": "Comentaris", "LANGUAGE": "Català", - "Languages:": "Llenguatges:", + "Languages:": "Idiomes:", "More posts about %s": "Més entrades sobre %s", - "Newer posts": "Entrades posteriors", + "Newer posts": "Entrades més recents", "Next post": "Entrada següent", + "Next": "Següent", "No posts found.": "Publicació no trobada", "Nothing found.": "No trobat", - "Older posts": "Entrades anteriors", + "Older posts": "Entrades més antigues", "Original site": "Lloc original", "Posted:": "Publicat:", "Posts about %s": "Entrades sobre %s", - "Posts by %s": "", + "Posts by %s": "Entrades per %s", "Posts for year %s": "Entrades de l'any %s", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "Publicat en {month} {year}", + "Posts for {month_day_year}": "Entrades per {month_day_year}", + "Posts for {month_year}": "Publicat en {month_year}", "Previous post": "Entrada anterior", + "Previous": "Anterior", "Publication date": "Data de publicació", - "RSS feed": "Feed RSS", + "RSS feed": "Canal RSS", "Read in English": "Llegeix-ho en català", "Read more": "Llegeix-ne més", "Skip to main content": "Vés al comentari principal", "Source": "Codi", - "Subcategories:": "", - "Tags and Categories": "Etiquetes i Categories", + "Subcategories:": "Subcategories:", + "Tags and Categories": "Etiquetes i categories", "Tags": "Etiquetes", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", - "Write your page here.": "", - "Write your post here.": "", + "Toggle navigation": "Commuta la navegació", + "Uncategorized": "Sense categoria", + "Up": "Amunt", + "Updates": "Actualitzacions", + "Write your page here.": "Escriviu la vostra pàgina aquí.", + "Write your post here.": "Escriviu la vostra entrada aquí.", "old posts, page %d": "entrades antigues, pàgina %d", "page %d": "pàgina %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_cs.py b/nikola/data/themes/base/messages/messages_cs.py index 42fb1c1..dcff7e6 100644 --- a/nikola/data/themes/base/messages/messages_cs.py +++ b/nikola/data/themes/base/messages/messages_cs.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min zbývajících", "(active)": "", "Also available in:": "Dostupné také v", "Archive": "Archiv", + "Atom feed": "", "Authors": "", "Categories": "Kategorie", "Comments": "Komentáře", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Další příspěvky o %s", "Newer posts": "Novější příspěvky", "Next post": "Další příspěvek", + "Next": "", "No posts found.": "Nebyly nalezeny žádné příspěvky.", "Nothing found.": "Nic nebylo nalezeno.", "Older posts": "Starší příspěvky", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Příspěvky o %s", "Posts by %s": "", "Posts for year %s": "Příspěvky v roce %s", - "Posts for {month} {day}, {year}": "Příspěvky v {month} {day}, {year}", - "Posts for {month} {year}": "Příspěvky v {month} {year}", + "Posts for {month_day_year}": "Příspěvky v {month_day_year}", + "Posts for {month_year}": "Příspěvky v {month_year}", "Previous post": "Předchozí příspěvek", + "Previous": "", "Publication date": "Datum zveřejnění", "RSS feed": "RSS zdroj", "Read in English": "Číst v češtině", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Štítky", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "", "Write your post here.": "", "old posts, page %d": "staré příspěvky, strana %d", "page %d": "strana %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_da.py b/nikola/data/themes/base/messages/messages_da.py index 08b3d15..b070db5 100644 --- a/nikola/data/themes/base/messages/messages_da.py +++ b/nikola/data/themes/base/messages/messages_da.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min. tilbage at læse", "(active)": "", "Also available in:": "Fås også i:", "Archive": "Arkiv", + "Atom feed": "", "Authors": "", "Categories": "Kategorier", "Comments": "Kommentarer", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Yderligere indlæg om %s", "Newer posts": "Nyere indlæg", "Next post": "Næste indlæg", + "Next": "", "No posts found.": "Søgningen gav ingen resultater.", "Nothing found.": "Søgningen gav ingen resultater.", "Older posts": "Ældre indlæg", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Indlæg om %s", "Posts by %s": "", "Posts for year %s": "Indlæg for %s", - "Posts for {month} {day}, {year}": "Indlæs for {month} {day}, {year}", - "Posts for {month} {year}": "Indlæg for {month} {year}", + "Posts for {month_day_year}": "Indlæs for {month_day_year}", + "Posts for {month_year}": "Indlæg for {month_year}", "Previous post": "Tidligere indlæg", + "Previous": "", "Publication date": "Udgivelsesdato", "RSS feed": "RSS-nyhedskilde", "Read in English": "Læs på dansk", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Nøgleord", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "", "Write your post here.": "", "old posts, page %d": "gamle indlæg, side %d", "page %d": "side %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py index 7981c69..1ba4dd4 100644 --- a/nikola/data/themes/base/messages/messages_de.py +++ b/nikola/data/themes/base/messages/messages_de.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min verbleiben zum Lesen", "(active)": "(aktiv)", "Also available in:": "Auch verfügbar in:", "Archive": "Archiv", + "Atom feed": "Atom-Feed", "Authors": "Autoren", "Categories": "Kategorien", "Comments": "Kommentare", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Weitere Einträge über %s", "Newer posts": "Neuere Einträge", "Next post": "Nächster Eintrag", + "Next": "Nächster Eintrag", "No posts found.": "Keine Einträge gefunden.", "Nothing found.": "Nichts gefunden.", "Older posts": "Ältere Einträge", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Einträge über %s", "Posts by %s": "Einträge von %s", "Posts for year %s": "Einträge aus dem Jahr %s", - "Posts for {month} {day}, {year}": "Einträge vom {day}. {month} {year}", - "Posts for {month} {year}": "Einträge aus {month} {year}", + "Posts for {month_day_year}": "Einträge vom {month_day_year}", + "Posts for {month_year}": "Einträge aus {month_year}", "Previous post": "Vorheriger Eintrag", + "Previous": "Vorheriger Eintrag", "Publication date": "Veröffentlichungsdatum", "RSS feed": "RSS-Feed", "Read in English": "Auf Deutsch lesen", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Unterkategorien:", "Tags and Categories": "Tags und Kategorien", "Tags": "Tags", - "Toggle navigation": "", + "Toggle navigation": "Navigation umschalten", "Uncategorized": "Nicht kategorisiert", + "Up": "Nach oben", "Updates": "Updates", "Write your page here.": "Schreibe hier deinen Seiteninhalt hin.", "Write your post here.": "Schreibe hier deinen Eintrag hin.", "old posts, page %d": "Ältere Einträge, Seite %d", "page %d": "Seite %d", + "updated": "aktualisiert", } diff --git a/nikola/data/themes/base/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py index 7e32afa..7021d84 100644 --- a/nikola/data/themes/base/messages/messages_el.py +++ b/nikola/data/themes/base/messages/messages_el.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "", "(active)": "", "Also available in:": "Διαθέσιμο και στα:", "Archive": "Αρχείο", + "Atom feed": "", "Authors": "", "Categories": "Κατηγορίες", "Comments": "Σχόλια", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Περισσότερες αναρτήσεις για %s", "Newer posts": "Νεότερες αναρτήσεις", "Next post": "Επόμενη ανάρτηση", + "Next": "", "No posts found.": "Δε βρέθηκαν αναρτήσεις", "Nothing found.": "Δε βρέθηκε περιεχόμενο", "Older posts": "Παλαιότερες αναρτήσεις", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Αναρτήσεις για %s", "Posts by %s": "", "Posts for year %s": "Αναρτήσεις για το έτος %s", - "Posts for {month} {day}, {year}": "Αναρτήσεις στις {day} {month}, {year}", - "Posts for {month} {year}": "Αναρτήσεις για τον {month} του {year}", + "Posts for {month_day_year}": "Αναρτήσεις στις {month_day_year}", + "Posts for {month_year}": "Αναρτήσεις για τον {month_year}", "Previous post": "Προηγούμενη ανάρτηση", + "Previous": "", "Publication date": "Ημερομηνία δημοσίευσης", "RSS feed": "", "Read in English": "Διαβάστε στα Ελληνικά", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Ετικέτες", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "", "Write your post here.": "", "old posts, page %d": "σελίδα παλαιότερων αναρτήσεων %d", "page %d": "σελίδα %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py index 3c39f55..a6beb5e 100644 --- a/nikola/data/themes/base/messages/messages_en.py +++ b/nikola/data/themes/base/messages/messages_en.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min remaining to read", "(active)": "(active)", "Also available in:": "Also available in:", "Archive": "Archive", + "Atom feed": "Atom feed", "Authors": "Authors", "Categories": "Categories", "Comments": "Comments", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "More posts about %s", "Newer posts": "Newer posts", "Next post": "Next post", + "Next": "Next", "No posts found.": "No posts found.", "Nothing found.": "Nothing found.", "Older posts": "Older posts", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Posts about %s", "Posts by %s": "Posts by %s", "Posts for year %s": "Posts for year %s", - "Posts for {month} {day}, {year}": "Posts for {month} {day}, {year}", - "Posts for {month} {year}": "Posts for {month} {year}", + "Posts for {month_day_year}": "Posts for {month_day_year}", + "Posts for {month_year}": "Posts for {month_year}", "Previous post": "Previous post", + "Previous": "Previous", "Publication date": "Publication date", "RSS feed": "RSS feed", "Read in English": "Read in English", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Tags", "Toggle navigation": "Toggle navigation", "Uncategorized": "Uncategorized", + "Up": "Up", "Updates": "Updates", "Write your page here.": "Write your page here.", "Write your post here.": "Write your post here.", "old posts, page %d": "old posts, page %d", "page %d": "page %d", + "updated": "updated", } diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py index 9d1ae72..2793ff5 100644 --- a/nikola/data/themes/base/messages/messages_eo.py +++ b/nikola/data/themes/base/messages/messages_eo.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minutoj por legi", "(active)": "(aktiva)", "Also available in:": "Ankaŭ disponebla en:", "Archive": "Arkivo", + "Atom feed": "Atom fluo", "Authors": "Aŭtoroj", "Categories": "Kategorioj", "Comments": "Komentoj", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Pli da artikoloj pri %s", "Newer posts": "Pli novaj artikoloj", "Next post": "Venonta artikolo", + "Next": "Venonta", "No posts found.": "Neniu artikoloj trovitaj.", "Nothing found.": "Nenio trovita.", "Older posts": "Pli malnovaj artikoloj", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Artikoloj pri %s", "Posts by %s": "Artikoloj de %s", "Posts for year %s": "Artikoloj de la jaro %s", - "Posts for {month} {day}, {year}": "Artikoloj de la {day}a de {month} {year}", - "Posts for {month} {year}": "Artikoloj de {month} {year}", + "Posts for {month_day_year}": "Artikoloj de la {month_day_year}", + "Posts for {month_year}": "Artikoloj de {month_year}", "Previous post": "Antaŭa artikolo", + "Previous": "Antaŭa", "Publication date": "Eldona dato", "RSS feed": "RSS fluo", "Read in English": "Legu ĝin en Esperanto", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etikedoj", "Toggle navigation": "Ŝalti menuon", "Uncategorized": "Sen kategorioj", + "Up": "Supren", "Updates": "Ĝisdatigoj", "Write your page here.": "Skribu tie vian paĝon.", "Write your post here.": "Skribu tie vian artikolon.", "old posts, page %d": "%da paĝo de malnovaj artikoloj", "page %d": "paĝo %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py index c590a64..bb58c82 100644 --- a/nikola/data/themes/base/messages/messages_es.py +++ b/nikola/data/themes/base/messages/messages_es.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "quedan %d minutos de lectura", "(active)": "(activo)", "Also available in:": "También disponible en:", "Archive": "Archivo", + "Atom feed": "Canal Atom", "Authors": "Autores", "Categories": "Categorías", "Comments": "Comentarios", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Más publicaciones sobre %s", "Newer posts": "Publicaciones posteriores", "Next post": "Siguiente publicación", + "Next": "Siguiente", "No posts found.": "No se encontraron publicaciones.", "Nothing found.": "No se encontró nada.", "Older posts": "Publicaciones anteriores", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Publicaciones sobre %s", "Posts by %s": "Publicaciones de %s", "Posts for year %s": "Publicaciones del año %s", - "Posts for {month} {day}, {year}": "Publicaciones del {day} de {month} de {year}", - "Posts for {month} {year}": "Posts de {month} de {year}", + "Posts for {month_day_year}": "Publicaciones del {month_day_year}", + "Posts for {month_year}": "Posts de {month_year}", "Previous post": "Publicación anterior", + "Previous": "Anterior", "Publication date": "Fecha de publicación", "RSS feed": "Canal RSS", "Read in English": "Leer en español", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etiquetas", "Toggle navigation": "Mostrar navegación", "Uncategorized": "Sin categoría", + "Up": "Arriba", "Updates": "Actualizaciones", "Write your page here.": "Escriba su página aquí.", "Write your post here.": "Escriba su publicación aquí.", "old posts, page %d": "publicaciones antiguas, página %d", "page %d": "página %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_et.py b/nikola/data/themes/base/messages/messages_et.py index 9b3ba9c..e2a6e30 100644 --- a/nikola/data/themes/base/messages/messages_et.py +++ b/nikola/data/themes/base/messages/messages_et.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "", - "(active)": "", + "%d min remaining to read": "%d minutit veel lugeda", + "(active)": "(aktiivne)", "Also available in:": "Saadaval ka:", "Archive": "Arhiiv", - "Authors": "", + "Atom feed": "Atom uudisvoog", + "Authors": "Autorid", "Categories": "Kategooriad", - "Comments": "", + "Comments": "Kommentaarid", "LANGUAGE": "Eesti", - "Languages:": "", + "Languages:": "Keeled:", "More posts about %s": "Veel postitusi %s kohta", "Newer posts": "Uued postitused", "Next post": "Järgmine postitus", - "No posts found.": "", - "Nothing found.": "", + "Next": "Järgmine", + "No posts found.": "Postitusi ei leitud.", + "Nothing found.": "Midagi ei leitud.", "Older posts": "Vanemad postitused", "Original site": "Algallikas", "Posted:": "Postitatud:", "Posts about %s": "Postitused %s kohta", - "Posts by %s": "", + "Posts by %s": "Postitus kasutajalt %s", "Posts for year %s": "Postitused aastast %s", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "Postitused {year} aasta kuust {month} ", + "Posts for {month_day_year}": "Postitused {month_day_year}", + "Posts for {month_year}": "Postitused {month_year}", "Previous post": "Eelmine postitus", - "Publication date": "", - "RSS feed": "", + "Previous": "Eelmine", + "Publication date": "Avaldamise kuupäev", + "RSS feed": "RSS uudisvoog", "Read in English": "Loe eesti keeles", "Read more": "Loe veel", - "Skip to main content": "", + "Skip to main content": "Otse peamise sisu juurde", "Source": "Lähtekood", - "Subcategories:": "", + "Subcategories:": "Alamkategooriad:", "Tags and Categories": "Sildid ja kategooriad", "Tags": "Märksõnad", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", - "Write your page here.": "", - "Write your post here.": "", + "Toggle navigation": "Näita või peida menüüd", + "Uncategorized": "Kategooriata", + "Up": "Üles", + "Updates": "Uuendused", + "Write your page here.": "Kirjuta oma lehekülje sisu siia.", + "Write your post here.": "Kirjuta oma postitus siia.", "old posts, page %d": "vanade postituste, leht %d", "page %d": "leht %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_eu.py b/nikola/data/themes/base/messages/messages_eu.py index 68d7162..dd39991 100644 --- a/nikola/data/themes/base/messages/messages_eu.py +++ b/nikola/data/themes/base/messages/messages_eu.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minutu gelditzen dira irakurtzeko", "(active)": "(aktibo)", "Also available in:": "Eskuragarria hizkuntza hauetan ere:", "Archive": "Artxiboa", + "Atom feed": "", "Authors": "Egileak", "Categories": "Kategoriak", "Comments": "Iruzkinak", @@ -14,17 +15,19 @@ MESSAGES = { "More posts about %s": "%s-ri buruzko argitalpen gehiago", "Newer posts": "Argitalpen berriagoak", "Next post": "Hurrengo argitalpena", + "Next": "", "No posts found.": "Ez da argitalpenik aurkitu", "Nothing found.": "Ez da ezer aurkitu", - "Older posts": "Post zaharragoak", - "Original site": "Jatorrizko orria", + "Older posts": "Argitalpen zaharragoak", + "Original site": "Jatorrizko gunea", "Posted:": "Argitaratuta:", "Posts about %s": "%s-ri buruzko argitalpenak", "Posts by %s": "%s-ek idatzitako argitalpenak", "Posts for year %s": "%s. urteko argitalpenak", - "Posts for {month} {day}, {year}": "{year}ko {month}aren {day}ko argitalpenak", - "Posts for {month} {year}": "{year}ko {month}ren argitalpenak", + "Posts for {month_day_year}": "{month_day_year} argitalpenak", + "Posts for {month_year}": "{month_year} argitalpenak", "Previous post": "Aurreko argitalpena", + "Previous": "", "Publication date": "Argitaratze-data", "RSS feed": "RSS jarioa", "Read in English": "Euskaraz irakurri", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etiketak", "Toggle navigation": "", "Uncategorized": "Kategorizatu-gabeak", + "Up": "", "Updates": "Eguneraketak", "Write your page here.": "Idatzi zure orria hemen", "Write your post here.": "Idatzi zure argitalpena hemen", - "old posts, page %d": "Argitalpen zaharragoak,%d. orria", + "old posts, page %d": "Argitalpen zaharragoak, %d. orria", "page %d": "%d. orria", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py index b2abad6..ade3276 100644 --- a/nikola/data/themes/base/messages/messages_fa.py +++ b/nikola/data/themes/base/messages/messages_fa.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d دقیقه برای خواندن باقی مانده", "(active)": "(فعال)", "Also available in:": "همچنین قابل دسترس از:", "Archive": "آرشیو", + "Atom feed": "", "Authors": "نویسندهها", "Categories": "دستهها", "Comments": "دیدگاهها", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "ارسالهای بیشتر دربارهٔ%s", "Newer posts": "ارسالهای جدیدتر", "Next post": "ارسال بعدی", + "Next": "بعدی", "No posts found.": "هیچ پستی پیدا نشد.", "Nothing found.": "هیچچیزی پیدا نشد.", "Older posts": "پستهای قدیمیتر", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "ارسالها دربارهٔ %s", "Posts by %s": "ارسالهای %s", "Posts for year %s": "ارسالها برای سال %s", - "Posts for {month} {day}, {year}": "ارسال برای {month} {day}. {year}", - "Posts for {month} {year}": "ارسال برای {month} {year}", + "Posts for {month_day_year}": "ارسال برای {month_day_year}", + "Posts for {month_year}": "ارسال برای {month_year}", "Previous post": "ارسال پیشین", + "Previous": "قبلی", "Publication date": "تاریخ انتشار", "RSS feed": "خوراک", "Read in English": "به فارسی بخوانید", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "برچسبها", "Toggle navigation": "", "Uncategorized": "دستهبندی نشده", + "Up": "", "Updates": "بروزرسانیها", "Write your page here.": "من صفحه را اینجا بنویسید. ", "Write your post here.": "متن پستتان را اینجا بنویسید.", "old posts, page %d": "صفحهٔ ارسالهای قدیمی %d", "page %d": "برگه %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py index 9a70ede..e29db48 100644 --- a/nikola/data/themes/base/messages/messages_fi.py +++ b/nikola/data/themes/base/messages/messages_fi.py @@ -1,30 +1,33 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minuuttia lukuaikaa", "(active)": "(aktiivinen)", "Also available in:": "Saatavilla myös:", "Archive": "Arkisto", + "Atom feed": "Atom-syöte", "Authors": "Kirjoittajat", "Categories": "Kategoriat", "Comments": "Kommentit", "LANGUAGE": "Suomi", "Languages:": "Kielet:", - "More posts about %s": "Lisää postauksia aiheesta %s", - "Newer posts": "Uudempia postauksia", - "Next post": "Seuraava postaus", - "No posts found.": "Postauksia ei löytynyt.", + "More posts about %s": "Lisää kirjoituksia aiheesta %s", + "Newer posts": "Uudempia kirjoituksia", + "Next post": "Seuraava kirjoitus", + "Next": "Seuraava", + "No posts found.": "Kirjoituksia ei löytynyt.", "Nothing found.": "Ei hakutuloksia.", - "Older posts": "Vanhempia postauksia", + "Older posts": "Vanhempia kirjoituksia", "Original site": "Alkuperäinen sivusto", - "Posted:": "Postattu:", - "Posts about %s": "Postauksia aiheesta %s", - "Posts by %s": "Postaukset kirjoittajalta %s", - "Posts for year %s": "Postauksia vuodelta %s", - "Posts for {month} {day}, {year}": "Kirjoituksia ajalta {day}. {month}ta {year}", - "Posts for {month} {year}": "Postauksia ajalle {month} {year}", - "Previous post": "Edellinen postaus", + "Posted:": "Kirjoitettu:", + "Posts about %s": "Kirjoituksia aiheesta %s", + "Posts by %s": "Artikkelit kirjoittajalta %s", + "Posts for year %s": "Kirjoituksia vuodelta %s", + "Posts for {month_day_year}": "Kirjoituksia ajalta {month_day_year}", + "Posts for {month_year}": "Kirjoituksia ajalta {month_year}", + "Previous post": "Edellinen kirjoitus", + "Previous": "Edellinen", "Publication date": "Julkaisupäivämäärä", "RSS feed": "RSS-syöte", "Read in English": "Lue suomeksi", @@ -32,13 +35,15 @@ MESSAGES = { "Skip to main content": "Hyppää sisältöön", "Source": "Lähde", "Subcategories:": "Alakategoriat:", - "Tags and Categories": "Tagit ja kategoriat", - "Tags": "Tagit", - "Toggle navigation": "", + "Tags and Categories": "Avainsanat ja kategoriat", + "Tags": "Avainsanat", + "Toggle navigation": "Vaihda navigointia", "Uncategorized": "Luokittelematon", + "Up": "Ylös", "Updates": "Päivitykset", "Write your page here.": "Kirjoita sisältö tähän.", "Write your post here.": "Kirjoita sisältö tähän.", - "old posts, page %d": "vanhoja postauksia, sivu %d", + "old posts, page %d": "vanhoja kirjoituksia, sivu %d", "page %d": "sivu %d", + "updated": "päivitetty", } diff --git a/nikola/data/themes/base/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py index 6be1422..971d607 100644 --- a/nikola/data/themes/base/messages/messages_fr.py +++ b/nikola/data/themes/base/messages/messages_fr.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "Il reste encore %d min. de lecture", "(active)": "(actif)", "Also available in:": "Également disponible en :", "Archive": "Archives", + "Atom feed": "Flux Atom", "Authors": "Auteurs", "Categories": "Catégories", "Comments": "Commentaires", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Plus d'articles sur %s", "Newer posts": "Articles récents", "Next post": "Article suivant", + "Next": "Article suivant", "No posts found.": "Pas d'articles.", "Nothing found.": "Pas de résultats.", "Older posts": "Anciens articles", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Articles sur %s", "Posts by %s": "Publiés par %s", "Posts for year %s": "Articles de l'année %s", - "Posts for {month} {day}, {year}": "Articles du {day} {month} {year}", - "Posts for {month} {year}": "Articles de {month} {year}", + "Posts for {month_day_year}": "Articles du {month_day_year}", + "Posts for {month_year}": "Articles de {month_year}", "Previous post": "Article précédent", + "Previous": "Article précédent", "Publication date": "Date de publication", "RSS feed": "Flux RSS", "Read in English": "Lire en français", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Sous-catégories", "Tags and Categories": "Étiquettes et catégories", "Tags": "Étiquettes", - "Toggle navigation": "", + "Toggle navigation": "Basculer en navigation", "Uncategorized": "Sans catégorie", + "Up": "Retour en haut", "Updates": "Mises à jour", "Write your page here.": "Écrivez votre page ici.", "Write your post here.": "Écrivez votre billet ici.", "old posts, page %d": "anciens articles, page %d", "page %d": "page %d", + "updated": "mis à jour", } diff --git a/nikola/data/themes/base/messages/messages_fur.py b/nikola/data/themes/base/messages/messages_fur.py new file mode 100644 index 0000000..6f75b66 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_fur.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "a restin altris %d minûts di leture", + "(active)": "(atîf)", + "Also available in:": "Disponibil ancje in:", + "Archive": "Archivi", + "Atom feed": "Feed Atom", + "Authors": "Autôrs", + "Categories": "Categoriis", + "Comments": "Coments", + "LANGUAGE": "Furlan", + "Languages:": "Lenghis:", + "More posts about %s": "Altris articui su %s", + "Newer posts": "Articui plui resints", + "Next post": "Prossim articul", + "Next": "Prossim", + "No posts found.": "Nissun articul cjatât.", + "Nothing found.": "Nol è stât cjatât nuie.", + "Older posts": "Articui precedents", + "Original site": "Sît originâl", + "Posted:": "Publicât:", + "Posts about %s": "Articui su %s", + "Posts by %s": "Articui di %s", + "Posts for year %s": "Articui par an %s", + "Posts for {month_day_year}": "Articui par {month_day_year}", + "Posts for {month_year}": "Articui par {month_year}", + "Previous post": "Articul precedent", + "Previous": "Precedent", + "Publication date": "Date di publicazion", + "RSS feed": "Feed RSS", + "Read in English": "Lei par furlan", + "Read more": "Continue la leture", + "Skip to main content": "Va al test principâl", + "Source": "Document origjinâl", + "Subcategories:": "Subcategoriis:", + "Tags and Categories": "Tags e categoriis", + "Tags": "Tags", + "Toggle navigation": "Ativâ la navigazion", + "Uncategorized": "Cence categorie", + "Up": "Su", + "Updates": "Inzornaments", + "Write your page here.": "Scrîf ca la tô pagjine.", + "Write your post here.": "Scrîf ca il to articul.", + "old posts, page %d": "articui vecjos, pagjine %d", + "page %d": "pagjine %d", + "updated": "inzornât", +} diff --git a/nikola/data/themes/base/messages/messages_gl.py b/nikola/data/themes/base/messages/messages_gl.py index 11ac1cf..c41d61f 100644 --- a/nikola/data/themes/base/messages/messages_gl.py +++ b/nikola/data/themes/base/messages/messages_gl.py @@ -1,19 +1,21 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min restantes para ler", "(active)": "(activo)", "Also available in:": "Tamén dispoñible en:", "Archive": "Arquivo", + "Atom feed": "", "Authors": "Autores", "Categories": "Categorías", "Comments": "Comentarios", - "LANGUAGE": "Inglés", + "LANGUAGE": "Galego", "Languages:": "Linguas:", "More posts about %s": "Máis artigos sobre %s", "Newer posts": "Últimos artigos", "Next post": "Seguinte artigo", + "Next": "", "No posts found.": "Non se atoparon artigos.", "Nothing found.": "Non se atopou nada.", "Older posts": "Artigos vellos", @@ -22,12 +24,13 @@ MESSAGES = { "Posts about %s": "Artigos sobre %s", "Posts by %s": "Publicacións de %s", "Posts for year %s": "Artigos do ano %s", - "Posts for {month} {day}, {year}": "Artigos de {month} {day}, {year}", - "Posts for {month} {year}": "Artigos de {month} {year}", + "Posts for {month_day_year}": "Artigos de {month_day_year}", + "Posts for {month_year}": "Artigos de {month_year}", "Previous post": "Artigo anterior", + "Previous": "", "Publication date": "Data de publicación", "RSS feed": "Sindicación RSS", - "Read in English": "Ler en Inglés", + "Read in English": "Ler en Galego", "Read more": "Ler máis", "Skip to main content": "Saltar ó contido principal", "Source": "Fonte", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etiquetas", "Toggle navigation": "", "Uncategorized": "Sen categoría", + "Up": "", "Updates": "Actualizacións", "Write your page here.": "Escribe a túa páxina aquí.", "Write your post here.": "Escribe o teu artigo aquí.", "old posts, page %d": "Artigos vellos, páxina %d", "page %d": "páxina %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_he.py b/nikola/data/themes/base/messages/messages_he.py index 106e35e..2de9796 100644 --- a/nikola/data/themes/base/messages/messages_he.py +++ b/nikola/data/themes/base/messages/messages_he.py @@ -1,30 +1,33 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "d% דקות נותרים לסיום קריאה", + "%d min remaining to read": "%d דקות נותרים לסיום קריאה", "(active)": "(פעיל)", "Also available in:": "זמין גם ב:", "Archive": "ארכיב", + "Atom feed": "", "Authors": "מחברים", "Categories": "קטגוריות", "Comments": "הערות", "LANGUAGE": "אנגלית", "Languages:": "שפות:", - "More posts about %s": "עוד פוסטים אודות s%", + "More posts about %s": "עוד פוסטים אודות %s", "Newer posts": "פוסטים חדשים", "Next post": "לפוסט הבא", + "Next": "", "No posts found.": "לא נמצאו פוסטים", "Nothing found.": "לא נמצא", "Older posts": "פוסטים ישנים", "Original site": "אתר המקורי", "Posted:": "פורסם:", - "Posts about %s": "פוסטים אודות s%", - "Posts by %s": "פוסטים ע״י s%", - "Posts for year %s": "פוסטים לשנת s%", - "Posts for {month} {day}, {year}": "פוסטים עבוד {year},{day}{month}", - "Posts for {month} {year}": "פוסטים עבוד {year}{month}", + "Posts about %s": "פוסטים אודות %s", + "Posts by %s": "פוסטים ע״י %s", + "Posts for year %s": "פוסטים לשנת %s", + "Posts for {month_day_year}": "פוסטים עבוד {month_day_year}", + "Posts for {month_year}": "פוסטים עבוד {month_year}", "Previous post": "פוסט הקודם", + "Previous": "", "Publication date": "תאריך פרסום", "RSS feed": "פיד RSS", "Read in English": "קרא באנגלית", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "תגים", "Toggle navigation": "החלף מצב ניווט", "Uncategorized": "לא משויך לקטגוריה", + "Up": "", "Updates": "עדכונים", "Write your page here.": "תכתוב את העמוד שלך פה.", "Write your post here.": "תכתוב את הפוסט שלך פה.", - "old posts, page %d": "פוסטים קודמים, דף d%", - "page %d": "עמוד d%", + "old posts, page %d": "פוסטים קודמים, דף %d", + "page %d": "עמוד %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_hi.py b/nikola/data/themes/base/messages/messages_hi.py index 551a9df..25c3d2e 100644 --- a/nikola/data/themes/base/messages/messages_hi.py +++ b/nikola/data/themes/base/messages/messages_hi.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "पढ़ने में %d मिनट बाकी", + "%d min remaining to read": "पढ़ने के लिए %d मिनट शेष", "(active)": "(सक्रिय)", "Also available in:": "उपलब्ध भाषाएँ:", - "Archive": "आर्काइव", + "Archive": "संग्रह", + "Atom feed": "एटम फीड", "Authors": "लेखक", "Categories": "श्रेणियाँ", "Comments": "टिप्पणियाँ", "LANGUAGE": "हिन्दी", "Languages:": "भाषाएँ:", - "More posts about %s": "%s के बारे में अौर पोस्टें", - "Newer posts": "नई पोस्टें", - "Next post": "अगली पोस्ट", - "No posts found.": "कोई पोस्ट नहीं मिल सकी", - "Nothing found.": "कुछ नहीं मिल सका", - "Older posts": "पुरानी पोस्टें", - "Original site": "असली साइट", + "More posts about %s": "%s के बारे में और पोस्ट", + "Newer posts": "नई पोस्ट", + "Next post": "अगला पोस्ट", + "Next": "अगला", + "No posts found.": "कोई पोस्ट नहीं मिला", + "Nothing found.": "कुछ नहीं मिला", + "Older posts": "पुरानी पोस्ट", + "Original site": "मूल साइट", "Posted:": "पोस्टेड:", - "Posts about %s": "%s के बारे में पोस्टें", - "Posts by %s": "%s की पोस्टें", - "Posts for year %s": "साल %s की पोस्टें", - "Posts for {month} {day}, {year}": "{day} {month} {year} की पोस्टें", - "Posts for {month} {year}": "{month} {year} की पोस्टें", + "Posts about %s": "%s के बारे में पोस्ट", + "Posts by %s": "%s द्वारा पोस्ट", + "Posts for year %s": "वर्ष %s के पोस्ट", + "Posts for {month_day_year}": "{month_day_year} के पोस्ट", + "Posts for {month_year}": "{month_year} के पोस्ट", "Previous post": "पिछली पोस्ट", - "Publication date": "प्रकाशन की तारीख", + "Previous": "पिछला", + "Publication date": "प्रकाशन तिथि", "RSS feed": "आर एस एस फ़ीड", "Read in English": "हिन्दी में पढ़िए", "Read more": "और पढ़िए", - "Skip to main content": "मुख्य सामग्री पर जाएँ", - "Source": "सोर्स", + "Skip to main content": "मुख्य विषयवस्तु में जाएं", + "Source": "स्रोत", "Subcategories:": "उपश्रेणी", "Tags and Categories": "टैग्स और श्रेणियाँ", "Tags": "टैग्स", - "Toggle navigation": "", - "Uncategorized": "बिना श्रेणी", + "Toggle navigation": "टॉगल नेविगेशन", + "Uncategorized": "अवर्गीकृत", + "Up": "ऊपर", "Updates": "अपडेट्स", - "Write your page here.": "अपना पेज यहाँ लिखिए", - "Write your post here.": "अपनी पोस्ट यहाँ लिखिए", - "old posts, page %d": "पुरानी पोस्टें, पृष्ठ %d", + "Write your page here.": "अपना पेज यहाँ लिखें", + "Write your post here.": "अपनी पोस्ट यहाँ लिखें", + "old posts, page %d": "पुराने पोस्ट, पृष्ठ %d", "page %d": "पृष्ठ %d", + "updated": "संशोधित", } diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py index 933bafc..7c72f3b 100644 --- a/nikola/data/themes/base/messages/messages_hr.py +++ b/nikola/data/themes/base/messages/messages_hr.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minuta preostalo za čitanje", "(active)": "(aktivno)", "Also available in:": "Također dostupno i u:", "Archive": "Arhiva", + "Atom feed": "", "Authors": "Autori", "Categories": "Kategorije", "Comments": "Komentari", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Više postova o %s", "Newer posts": "Noviji postovi", "Next post": "Sljedeći post", + "Next": "", "No posts found.": "Nema postova.", "Nothing found.": "Nema ničeg.", "Older posts": "Stariji postovi", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Postovi o %s", "Posts by %s": "Objave od %s", "Posts for year %s": "Postovi za godinu %s", - "Posts for {month} {day}, {year}": "Objave za {month} {day}, {year}", - "Posts for {month} {year}": "Postovi za {month} {year}", + "Posts for {month_day_year}": "Postovi za {month_day_year}", + "Posts for {month_year}": "Postovi za {month_year}", "Previous post": "Prethodni post", + "Previous": "", "Publication date": "Nadnevak objave", "RSS feed": "RSS kanal", "Read in English": "Čitaj na hrvatskom", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Tagovi", "Toggle navigation": "", "Uncategorized": "Nekategorizirano", + "Up": "", "Updates": "Nadopune", "Write your page here.": "Napiši svoju stranicu ovdje", "Write your post here.": "Napiši svoju objavu ovdje", "old posts, page %d": "stari postovi, stranice %d", "page %d": "stranice %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_hu.py b/nikola/data/themes/base/messages/messages_hu.py index 0b137fc..8b0ac86 100644 --- a/nikola/data/themes/base/messages/messages_hu.py +++ b/nikola/data/themes/base/messages/messages_hu.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d perc van hátra olvasni", "(active)": "(aktív)", "Also available in:": "Olvasható még:", "Archive": "Archív", + "Atom feed": "Atom", "Authors": "Szerzők", "Categories": "Kategóriák", "Comments": "Hozzászólások", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Több bejegyzés erről: %s", "Newer posts": "Újabb bejegyzések", "Next post": "A következő bejegyzés", + "Next": "Következő", "No posts found.": "Nincs ilyen bejegyzés.", "Nothing found.": "Nincs találat.", "Older posts": "Régebbi bejegyzések", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Bejegyzések erről: %s", "Posts by %s": "Bejegyzések %s által", "Posts for year %s": "%s. bejegyzések", - "Posts for {month} {day}, {year}": "{year}. {month}. {day}.-i bejegyzések", - "Posts for {month} {year}": "{year}. {month}.-i bejegyzések", + "Posts for {month_day_year}": "{month_day_year} bejegyzések", + "Posts for {month_year}": "{month_year} bejegyzések", "Previous post": "Az előző bejegyzés ", + "Previous": "Előző", "Publication date": "A megjelenés dátuma", "RSS feed": "RSS", "Read in English": "Olvass magyarul", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Címkék", "Toggle navigation": "", "Uncategorized": "Nincs kategorizálva", + "Up": "Fel", "Updates": "Frissítések", "Write your page here.": "Ide írd az oldalad.", "Write your post here.": "Ide írd a bejegyzésed.", "old posts, page %d": "régi bejegyzések, %d. oldal", "page %d": "%d. oldal", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_ia.py b/nikola/data/themes/base/messages/messages_ia.py new file mode 100644 index 0000000..34868ba --- /dev/null +++ b/nikola/data/themes/base/messages/messages_ia.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "%dminutas de lectura remanente", + "(active)": "(active)", + "Also available in:": "Anque disponibile in:", + "Archive": "Archivo", + "Atom feed": "Fluxo Atom", + "Authors": "Authores", + "Categories": "Categorias", + "Comments": "Commentos", + "LANGUAGE": "Interlingua", + "Languages:": "Linguas:", + "More posts about %s": "Plure entratas super %s", + "Newer posts": "Entratas plus recente", + "Next post": "Entrata successive", + "Next": "Successive", + "No posts found.": "Nulle entrata esseva trovate.", + "Nothing found.": "Nihil esseva trovate.", + "Older posts": "Entratas plus vetule", + "Original site": "Sito original", + "Posted:": "Publicate:", + "Posts about %s": "Entratas super %s", + "Posts by %s": "Entratas per %s", + "Posts for year %s": "Entratas del anno %s", + "Posts for {month_day_year}": "Entratas de {month_day_year}", + "Posts for {month_year}": "Entratas de {month_year}", + "Previous post": "Entrata precedente", + "Previous": "Precendente", + "Publication date": "Data de publication", + "RSS feed": "Fluxo RSS", + "Read in English": "Lege in interlingua", + "Read more": "Lege plus", + "Skip to main content": "Salta al contento principal", + "Source": "Sorgente", + "Subcategories:": "Subcategorias:", + "Tags and Categories": "Etiquettas e categorias", + "Tags": "Etiquettas", + "Toggle navigation": "Commuta navigation", + "Uncategorized": "Sin categoria", + "Up": "In alto", + "Updates": "Actualisationes", + "Write your page here.": "Scribe tu pagina hic.", + "Write your post here.": "Scribe tu entrata hic.", + "old posts, page %d": "Vetule entratas, pagina %d", + "page %d": "pagina %d", + "updated": "actualisate", +} diff --git a/nikola/data/themes/base/messages/messages_id.py b/nikola/data/themes/base/messages/messages_id.py index ec60f9a..d6c53ae 100644 --- a/nikola/data/themes/base/messages/messages_id.py +++ b/nikola/data/themes/base/messages/messages_id.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d menit tersisa untuk membaca", "(active)": "(aktif)", "Also available in:": "Juga tersedia dalam:", "Archive": "Arsip", + "Atom feed": "Umpan Atom", "Authors": "Penulis", "Categories": "Kategori", "Comments": "Komentar", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Lebih banyak tulisan tentang %s", "Newer posts": "Tulisan lebih baru", "Next post": "Tulisan berikutnya", + "Next": "Sesudah", "No posts found.": "Tidak ada tulisan yang ditemukan.", "Nothing found.": "Tidak ditemukan.", "Older posts": "Tulisan lebih lama", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Tulisan tentang %s", "Posts by %s": "Tulisan oleh %s", "Posts for year %s": "Tulisan untuk tahun %s", - "Posts for {month} {day}, {year}": "Tulisan untuk {month} {day}, {year}", - "Posts for {month} {year}": "Tulisan untuk {month} {year}", + "Posts for {month_day_year}": "Tulisan untuk {month_day_year}", + "Posts for {month_year}": "Tulisan untuk {month_year}", "Previous post": "Tulisan sebelumnya", + "Previous": "Sebelum", "Publication date": "Tanggal publikasi", "RSS feed": "Sindikasi RSS", "Read in English": "Baca dalam Bahasa Indonesia", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Sub kategori:", "Tags and Categories": "Tag dan Kategori", "Tags": "Tag", - "Toggle navigation": "", + "Toggle navigation": "Alih navigasi", "Uncategorized": "Tanpa kategori", + "Up": "Atas", "Updates": "Update", "Write your page here.": "Tulis halaman Anda disini.", "Write your post here.": "Tulis tulisan Anda disini.", "old posts, page %d": "tulisan lama, halaman %d", "page %d": "halaman %d", + "updated": "diperbarui", } diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py index 08a65d5..2af1a62 100644 --- a/nikola/data/themes/base/messages/messages_it.py +++ b/nikola/data/themes/base/messages/messages_it.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "ulteriori %d minuti di lettura", "(active)": "(attivo)", "Also available in:": "Disponibile anche in:", "Archive": "Archivio", + "Atom feed": "Feed Atom", "Authors": "Autori", "Categories": "Categorie", "Comments": "Commenti", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Altri articoli collegati %s", "Newer posts": "Articoli più recenti", "Next post": "Articolo successivo", + "Next": "Successivo", "No posts found.": "Nessun articolo trovato.", "Nothing found.": "Non trovato.", "Older posts": "Articoli precedenti", @@ -22,23 +24,26 @@ MESSAGES = { "Posts about %s": "Articoli su %s", "Posts by %s": "Articoli di %s", "Posts for year %s": "Articoli per l'anno %s", - "Posts for {month} {day}, {year}": "Articoli per il {day} {month} {year}", - "Posts for {month} {year}": "Articoli per {month} {year}", + "Posts for {month_day_year}": "Articoli per il {month_day_year}", + "Posts for {month_year}": "Articoli per {month_year}", "Previous post": "Articolo precedente", + "Previous": "Precedente", "Publication date": "Data di pubblicazione", "RSS feed": "Feed RSS", - "Read in English": "Leggi in inglese", + "Read in English": "Leggi in italiano", "Read more": "Continua la lettura", "Skip to main content": "Vai al testo principale", "Source": "Sorgente", "Subcategories:": "Sottocategorie:", "Tags and Categories": "Tag e categorie", "Tags": "Tag", - "Toggle navigation": "", + "Toggle navigation": "Attiva la navigazione", "Uncategorized": "Senza categorie", + "Up": "Su", "Updates": "Aggiornamenti", "Write your page here.": "Scrivi qui la tua pagina.", "Write your post here.": "Scrivi qui il tuo post.", "old posts, page %d": "vecchi articoli, pagina %d", "page %d": "pagina %d", + "updated": "aggiornato", } diff --git a/nikola/data/themes/base/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py index f0d752e..fd563ad 100644 --- a/nikola/data/themes/base/messages/messages_ja.py +++ b/nikola/data/themes/base/messages/messages_ja.py @@ -1,30 +1,33 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "残りを読むのに必要な時間は%d分", "(active)": "(有効)", "Also available in:": "他の言語で読む:", - "Archive": "文書一覧", + "Archive": "過去記事一覧", + "Atom feed": "Atomフィード", "Authors": "著者一覧", "Categories": "カテゴリ", "Comments": "コメント", "LANGUAGE": "日本語", "Languages:": "言語:", - "More posts about %s": "%sに関する文書一覧", - "Newer posts": "新しい文書", - "Next post": "次の文書", - "No posts found.": "文書はありません。", + "More posts about %s": "%sに関する記事一覧", + "Newer posts": "新しい記事", + "Next post": "次の記事", + "Next": "次", + "No posts found.": "記事はありません。", "Nothing found.": "なにも見つかりませんでした。", - "Older posts": "過去の文書", + "Older posts": "過去の記事", "Original site": "翻訳元のサイト", "Posted:": "公開日時:", - "Posts about %s": "%sについての文書", - "Posts by %s": "%sの文書一覧", - "Posts for year %s": "%s年の文書", - "Posts for {month} {day}, {year}": "{year}年{month}{day}日の文書", - "Posts for {month} {year}": "{year}年{month}の文書", - "Previous post": "一つ前の文書", + "Posts about %s": "%sについての記事", + "Posts by %s": "%sの記事一覧", + "Posts for year %s": "%s年の記事", + "Posts for {month_day_year}": "{month_day_year}の記事", + "Posts for {month_year}": "{month_year}の記事", + "Previous post": "一つ前の記事", + "Previous": "前", "Publication date": "公開日", "RSS feed": "RSSフィード", "Read in English": "日本語で読む", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "サブカテゴリ", "Tags and Categories": "カテゴリおよびタグ一覧", "Tags": "タグ", - "Toggle navigation": "", - "Uncategorized": "uncategorized", + "Toggle navigation": "ナビゲーションを隠す", + "Uncategorized": "カテゴリなし", + "Up": "上", "Updates": "フィード", - "Write your page here.": "ここに文書を記述してください。", - "Write your post here.": "ここに文書を記述してください。", - "old posts, page %d": "過去の文書 %dページ目", + "Write your page here.": "ここに記事を記述してください。", + "Write your post here.": "ここに記事を記述してください。", + "old posts, page %d": "過去の記事 %dページ目", "page %d": "ページ%d", + "updated": "更新日時", } diff --git a/nikola/data/themes/base/messages/messages_ko.py b/nikola/data/themes/base/messages/messages_ko.py index 9a87aef..57b6cb9 100644 --- a/nikola/data/themes/base/messages/messages_ko.py +++ b/nikola/data/themes/base/messages/messages_ko.py @@ -1,19 +1,21 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "읽기 %d분 남음.", "(active)": "(활성됨)", "Also available in:": "이곳에서도 가능함:", "Archive": "저장소", + "Atom feed": "", "Authors": "작성자", "Categories": "분류", "Comments": "댓글", - "LANGUAGE": "영어", + "LANGUAGE": "한국어", "Languages:": "언어:", "More posts about %s": "%s에 대한 또다른 포스트", "Newer posts": "최신 포스트", "Next post": "다음 포스트", + "Next": "다음", "No posts found.": "검색된 포스트 없음.", "Nothing found.": "검색 결과 없음.", "Older posts": "옛날 포스트", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "%s에 대한 포스트", "Posts by %s": "%s에 의해 작성된 글", "Posts for year %s": "%s년도 포스트", - "Posts for {month} {day}, {year}": " {year}년 {month}월 {day}일에 작성된 포스트", - "Posts for {month} {year}": "{year}년 {month}월에 쓴 포스트", + "Posts for {month_day_year}": "{month_day_year}에 작성된 포스트", + "Posts for {month_year}": "{month_year}에 쓴 포스트", "Previous post": "이전 포스트", + "Previous": "이전", "Publication date": "발간일", "RSS feed": "RSS 목록", "Read in English": "영어로 읽기", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "태그", "Toggle navigation": "", "Uncategorized": "카테고리 없음", + "Up": "", "Updates": "업데이트", "Write your page here.": "여기에 페이지를 작성하세요.", "Write your post here.": "이곳에 글을 작성하세요.", "old posts, page %d": "이전 포스트, 페이지 %d", "page %d": "페이지 %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_lt.py b/nikola/data/themes/base/messages/messages_lt.py index 54b61d1..d79b940 100644 --- a/nikola/data/themes/base/messages/messages_lt.py +++ b/nikola/data/themes/base/messages/messages_lt.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "liko %d min skaitymo", "(active)": "(aktyvi)", "Also available in:": "Taip pat turimas šiomis kalbomis:", "Archive": "Archyvas", + "Atom feed": "", "Authors": "Autoriai", "Categories": "Kategorijos", "Comments": "Komentarai", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Daugiau įrašų apie %s", "Newer posts": "Naujesni įrašai", "Next post": "Sekantis įrašas", + "Next": "", "No posts found.": "Įrašų nerasta.", "Nothing found.": "Nieko nerasta.", "Older posts": "Senesni įrašai", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Įrašai apie %s", "Posts by %s": "Autoriaus %s įrašai", "Posts for year %s": "%s metų įrašai", - "Posts for {month} {day}, {year}": "{month} {day}, {year} įrašai", - "Posts for {month} {year}": "{month} {year} įrašai", + "Posts for {month_day_year}": "{month_day_year} įrašai", + "Posts for {month_year}": "{month_year} įrašai", "Previous post": "Ankstesnis įrašas", + "Previous": "", "Publication date": "Publikavimo data", "RSS feed": "RSS srautas", "Read in English": "Skaityti lietuviškai", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Žymės", "Toggle navigation": "", "Uncategorized": "Nekategorizuota", + "Up": "", "Updates": "Atnaujinimai", "Write your page here.": "Čia rašykite puslapio tekstą.", "Write your post here.": "Čia rašykite įrašo tekstą.", "old posts, page %d": "seni įrašai, %d puslapis", "page %d": "%d puslapis", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_mi.py b/nikola/data/themes/base/messages/messages_mi.py new file mode 100644 index 0000000..21f9739 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_mi.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "", + "(active)": "(mātātoa)", + "Also available in:": "", + "Archive": "Pūranga", + "Atom feed": "", + "Authors": "Kaituhi", + "Categories": "Kāwai", + "Comments": "Kōrerohia", + "LANGUAGE": " Māori", + "Languages:": "Reo", + "More posts about %s": "", + "Newer posts": "", + "Next post": "", + "Next": "Panuku", + "No posts found.": "", + "Nothing found.": "Kīhai i kitea", + "Older posts": "", + "Original site": "", + "Posted:": "", + "Posts about %s": "", + "Posts by %s": "", + "Posts for year %s": "", + "Posts for {month_day_year}": "", + "Posts for {month_year}": "", + "Previous post": "", + "Previous": "Whakamuri", + "Publication date": "", + "RSS feed": "Whāngai RSS", + "Read in English": "", + "Read more": "", + "Skip to main content": "", + "Source": "Matapuna", + "Subcategories:": "", + "Tags and Categories": "Tūtohu me nga kāwai", + "Tags": "Tūtohu", + "Toggle navigation": "", + "Uncategorized": "", + "Up": "Ki runga", + "Updates": "Whakahou", + "Write your page here.": "", + "Write your post here.": "", + "old posts, page %d": "", + "page %d": "whārangi %d", + "updated": "whakahoutia", +} diff --git a/nikola/data/themes/base/messages/messages_ml.py b/nikola/data/themes/base/messages/messages_ml.py new file mode 100644 index 0000000..a818320 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_ml.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "%d മിനിട്ട് കൂടി വായിച്ചു തീരാന് ", + "(active)": "(സജീവം)", + "Also available in:": "ലഭ്യമായ ഭാഷകള്:", + "Archive": "ആര്കൈവ്", + "Atom feed": "ആറ്റം ഫിഡ്", + "Authors": "രചയിതാക്കള്", + "Categories": "വിഭാഗങ്ങള്", + "Comments": "കമന്റുകള്", + "LANGUAGE": "മലയാളം", + "Languages:": "ഭാഷകള്:", + "More posts about %s": "%s-നെ കുറിച്ചുള്ള കൂടുതല് രചനകള്", + "Newer posts": "പുതിയ രചനകള്", + "Next post": "അടുത്ത രചന", + "Next": "അടുത്തത്", + "No posts found.": "രചനകള് ലഭ്യമല്ല.", + "Nothing found.": "ഒന്നും ലഭ്യമല്ല.", + "Older posts": "പഴയ രചനകള്", + "Original site": "യഥാര്ഥ സൈറ്റ്", + "Posted:": "എഴുതിയത്:", + "Posts about %s": "%s -നെ കുറിച്ചുള്ള രചനകള്", + "Posts by %s": "%s എഴുതിയ രചനകള്", + "Posts for year %s": "%s-ാം ആണ്ടിലെ രചനകള്", + "Posts for {month_day_year}": "{month_day_year} ലെ രചനകള്", + "Posts for {month_year}": "{month_year} ലെ രചനകള്", + "Previous post": "മുമ്പിലെ രചന", + "Previous": "കഴിഞ്ഞത്", + "Publication date": "പ്രസിദ്ധീകരിച്ച തീയതി", + "RSS feed": "ആര് എസ് എസ് ഫീഡ്", + "Read in English": "മലയാളത്തില് വായിക്കുക", + "Read more": "കൂടുതല് വായിക്കുക", + "Skip to main content": "പ്രധാന ഉള്ളടക്കത്തിലേക്ക് നേരെ പോവുക", + "Source": "സ്രോതസ്സ്", + "Subcategories:": "ഉപവിഭാഗങ്ങള്:", + "Tags and Categories": "ടാഗുകളും വിഭാഗങ്ങളും", + "Tags": "ടാഗുകള്", + "Toggle navigation": "നാവിഗേഷൻ മാറ്റുക", + "Uncategorized": "വേര്തിരിക്കാത്തവ", + "Up": "മുകളിലേക്ക്", + "Updates": "അപ്ഡേറ്റുകൾ", + "Write your page here.": "താങ്കളുടെ താള് ഇവിടെ എഴുതുക.", + "Write your post here.": "താങ്കളുടെ രചന ഇവിടെ എഴുതുക.", + "old posts, page %d": "പഴയ രചനകള്, താള് %d", + "page %d": "താള് %d", + "updated": "പുതുക്കിയത്", +} diff --git a/nikola/data/themes/base/messages/messages_mr.py b/nikola/data/themes/base/messages/messages_mr.py new file mode 100644 index 0000000..49e4fb6 --- /dev/null +++ b/nikola/data/themes/base/messages/messages_mr.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "वाचण्यासाठी %d मिनिटे शिल्लक आहेत", + "(active)": "(सक्रिय)", + "Also available in:": "मध्ये देखील उपलब्ध:", + "Archive": "संग्रह", + "Atom feed": "अॅटम फीड", + "Authors": "लेखक", + "Categories": "श्रेणी", + "Comments": "टिप्पण्या", + "LANGUAGE": "मराठी", + "Languages:": "भाषा:", + "More posts about %s": "%s बद्दल अधिक पोस्ट", + "Newer posts": "नवीन पोस्ट्स", + "Next post": "पुढील पोस्ट", + "Next": "पुढे", + "No posts found.": "कोणतीही पोस्ट आढळली नाहीत", + "Nothing found.": "काहीही सापडले नाही.", + "Older posts": "जुने पोस्ट", + "Original site": "मूळ साइट", + "Posted:": "पोस्ट केले:", + "Posts about %s": "%s बद्दल पोस्ट", + "Posts by %s": "%s द्वारा पोस्ट केलेले", + "Posts for year %s": "%s वर्षासाठी पोस्ट", + "Posts for {month_day_year}": "{month_day_year} साठी पोस्ट्स", + "Posts for {month_year}": "{month_year} साठी पोस्ट्स", + "Previous post": "मागील पोस्ट", + "Previous": "मागील", + "Publication date": "प्रकाशन तारीख", + "RSS feed": "आरएसएस फीड", + "Read in English": "मराठी मध्ये वाचा", + "Read more": "अधिक वाचा", + "Skip to main content": "मुख्य सामग्रीकडे जा", + "Source": "स्रोत", + "Subcategories:": "उपश्रेणी:", + "Tags and Categories": "टॅग्ज आणि श्रेण्या", + "Tags": "टॅग्ज", + "Toggle navigation": "नेव्हिगेशन टॉगल करा", + "Uncategorized": "अवर्गीकृत", + "Up": "वर", + "Updates": "अद्यतने", + "Write your page here.": "आपले पृष्ठ येथे लिहा.", + "Write your post here.": "आपले पोस्ट येथे लिहा.", + "old posts, page %d": "जुने पोस्ट, पृष्ठ %d", + "page %d": "पृष्ठ %d", + "updated": "अद्यतनित", +} diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py index 8ab0911..599d6ed 100644 --- a/nikola/data/themes/base/messages/messages_nb.py +++ b/nikola/data/themes/base/messages/messages_nb.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min gjenstår å lese", "(active)": "(aktiv)", "Also available in:": "Også tilgjengelig på:", "Archive": "Arkiv", + "Atom feed": "", "Authors": "", "Categories": "Kategorier", "Comments": "Kommentarer", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Flere innlegg om %s", "Newer posts": "Nyere innlegg", "Next post": "Neste innlegg", + "Next": "", "No posts found.": "Fant ingen innlegg.", "Nothing found.": "Fant ingenting.", "Older posts": "Eldre innlegg", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Innlegg om %s", "Posts by %s": "", "Posts for year %s": "Innlegg fra %s", - "Posts for {month} {day}, {year}": "Innlegg fra {day}. {month} {year}", - "Posts for {month} {year}": "Innlegg fra {month} {year}", + "Posts for {month_day_year}": "Innlegg fra {month_day_year}", + "Posts for {month_year}": "Innlegg fra {month_year}", "Previous post": "Forrige innlegg", + "Previous": "", "Publication date": "Publiseringsdato", "RSS feed": "RSS-nyhetskanal", "Read in English": "Les på norsk", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Merker", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "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", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py index 7c5698d..ee98121 100644 --- a/nikola/data/themes/base/messages/messages_nl.py +++ b/nikola/data/themes/base/messages/messages_nl.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min resterende leestijd ", "(active)": "(actief)", "Also available in:": "Ook beschikbaar in:", "Archive": "Archief", + "Atom feed": "Atom-feed", "Authors": "Auteurs", "Categories": "Categorieën", "Comments": "Commentaar", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Meer berichten over %s", "Newer posts": "Nieuwere berichten", "Next post": "Volgend bericht", + "Next": "Volgende", "No posts found.": "Geen berichten gevonden.", "Nothing found.": "Niets gevonden.", "Older posts": "Oudere berichten", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Berichten over %s", "Posts by %s": "Berichten van %s", "Posts for year %s": "Berichten voor het jaar %s", - "Posts for {month} {day}, {year}": "Berichten voor {month} {day}, {year}", - "Posts for {month} {year}": "Berichten voor {month} {year}", + "Posts for {month_day_year}": "Berichten voor {month_day_year}", + "Posts for {month_year}": "Berichten voor {month_year}", "Previous post": "Vorig bericht", + "Previous": "Vorige", "Publication date": "Publicatiedatum", "RSS feed": "RSS-feed", "Read in English": "Lees in het Nederlands", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Tags", "Toggle navigation": "Toggle navigatie", "Uncategorized": "Ongeordend", + "Up": "Omhoog", "Updates": "Bijgewerkte versies", "Write your page here.": "Schrijf hier je pagina.", "Write your post here.": "Schrijf hier je bericht.", "old posts, page %d": "oude berichten, pagina %d", "page %d": "pagina %d", + "updated": "bijgewerkt", } diff --git a/nikola/data/themes/base/messages/messages_pa.py b/nikola/data/themes/base/messages/messages_pa.py index 5eb76f1..8a0be76 100644 --- a/nikola/data/themes/base/messages/messages_pa.py +++ b/nikola/data/themes/base/messages/messages_pa.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "ਪੜਣ ਲਈ %d ਮਿੰਟ ਬਾਕੀ", "(active)": "(ਚਲੰਤ)", "Also available in:": "ਹੋਰ ਉਪਲਬਧ ਬੋਲੀਆਂ:", "Archive": "ਆਰਕਾਈਵ", + "Atom feed": "ਐਟਮ ਫੀਡ", "Authors": "ਲੇਖਕ", "Categories": "ਸ਼੍ਰੇਣੀ", "Comments": "ਟਿੱਪਣੀਆਂ", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "%s ਬਾਰੇ ਹੋਰ ਲਿਖਤਾਂ", "Newer posts": "ਨਵੀਆਂ ਲਿਖਤਾਂ", "Next post": "ਅਗਲੀ ਲਿਖਤ", + "Next": "ਅੱਗੇ ", "No posts found.": "ਕੋਈ ਲਿਖਤ ਨਹੀਂ ਲੱਭੀ |", "Nothing found.": "ਕੁਝ ਨਹੀਂ ਲੱਭਿਆ |", "Older posts": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "%s ਬਾਰੇ ਲਿਖਤਾਂ", "Posts by %s": "%s ਦੀ ਲਿਖਤਾਂ", "Posts for year %s": "ਸਾਲ %s ਦੀਆਂ ਲਿਖਤਾਂ", - "Posts for {month} {day}, {year}": "{day} {month} {year} ਦੀਆਂ ਲਿਖਤਾਂ", - "Posts for {month} {year}": "{month} {year} ਦੀਆਂ ਲਿਖਤਾਂ", + "Posts for {month_day_year}": "{month_day_year} ਦੀਆਂ ਲਿਖਤਾਂ", + "Posts for {month_year}": "{month_year} ਦੀਆਂ ਲਿਖਤਾਂ", "Previous post": "ਪਿਛਲੀ ਲਿਖਤ", + "Previous": "ਪਿੱਛੇ ", "Publication date": "ਛਪਾਈ ਦੀ ਤਰੀਕ", "RSS feed": "ਆਰ ਐੱਸ ਐੱਸ ਫੀਡ", "Read in English": "ਪੰਜਾਬੀ ਵਿੱਚ ਪੜ੍ਹੋ", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "ਉਪਸ਼੍ਰੇਣੀਆਂ:", "Tags and Categories": "ਟੈਗ ਅਤੇ ਸ਼੍ਰੇਣੀਆਂ", "Tags": "ਟੈਗ", - "Toggle navigation": "", + "Toggle navigation": "ਨੈਵੀਗੇਸ਼ਨ ਬਦਲੋ ", "Uncategorized": "ਇਤਾਹਾਸ", + "Up": "ਉੱਤੇ ", "Updates": "ਅੱਪਡੇਟਸ", "Write your page here.": "ਆਪਣਾ ਸਫ਼ਾ ਏਥੇ ਲਿਖੋ |", "Write your post here.": "ਆਪਣੀ ਲਿਖਤ ਏਥੇ ਲਿਖੋ |", "old posts, page %d": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ , ਸਫ਼ਾ %d", "page %d": "ਸਫ਼ਾ %d", + "updated": "ਅੱਪਡੇਟ ਕੀਤਾ", } diff --git a/nikola/data/themes/base/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py index 257a31a..2d13e32 100644 --- a/nikola/data/themes/base/messages/messages_pl.py +++ b/nikola/data/themes/base/messages/messages_pl.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "zostało %d minut czytania", "(active)": "(aktywne)", "Also available in:": "Również dostępny w językach:", "Archive": "Archiwum", + "Atom feed": "Kanał Atom", "Authors": "Autorzy", "Categories": "Kategorie", "Comments": "Komentarze", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Więcej postów o %s", "Newer posts": "Nowsze posty", "Next post": "Następny post", + "Next": "Następne", "No posts found.": "Nie znaleziono żadnych postów.", "Nothing found.": "Nic nie znaleziono.", "Older posts": "Starsze posty", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Posty o %s", "Posts by %s": "Posty autora %s", "Posts for year %s": "Posty z roku %s", - "Posts for {month} {day}, {year}": "Posty z {day} {month} {year}", - "Posts for {month} {year}": "Posty z {month} {year}", + "Posts for {month_day_year}": "Posty z {month_day_year}", + "Posts for {month_year}": "Posty z {month_year:MMMM yyyy}", "Previous post": "Poprzedni post", + "Previous": "Poprzednie", "Publication date": "Data publikacji", "RSS feed": "Kanał RSS", "Read in English": "Czytaj po polsku", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Tagi", "Toggle navigation": "Pokaż/ukryj menu", "Uncategorized": "Nieskategoryzowane", + "Up": "Do góry", "Updates": "Aktualności", "Write your page here.": "Tu wpisz treść strony.", "Write your post here.": "Tu wpisz treść postu.", "old posts, page %d": "stare posty, strona %d", "page %d": "strona %d", + "updated": "aktualizacja", } diff --git a/nikola/data/themes/base/messages/messages_pt.py b/nikola/data/themes/base/messages/messages_pt.py index 4a184bd..92c56a2 100644 --- a/nikola/data/themes/base/messages/messages_pt.py +++ b/nikola/data/themes/base/messages/messages_pt.py @@ -1,12 +1,13 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minutos restante para leitura", "(active)": "(ativo)", "Also available in:": "Também disponível em:", "Archive": "Arquivo", - "Authors": "", + "Atom feed": "Feed Atom", + "Authors": "Autores", "Categories": "Categorias", "Comments": "Comentários", "LANGUAGE": "Português", @@ -14,17 +15,19 @@ MESSAGES = { "More posts about %s": "Mais textos publicados sobre %s", "Newer posts": "Textos publicados mais recentes", "Next post": "Próximo texto publicado", + "Next": "Próximo", "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 by %s": "", + "Posts by %s": "Textos publicados por %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}", + "Posts for {month_day_year}": "Textos publicados de {month_day_year}", + "Posts for {month_year}": "Textos publicados de {month_year}", "Previous post": "Texto publicado anterior", + "Previous": "Anterior", "Publication date": "Data de publicação", "RSS feed": "Feed RSS", "Read in English": "Ler em português", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Sub-Categorias:", "Tags and Categories": "Etiquetas e Categorias", "Tags": "Etiqueta", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", + "Toggle navigation": "Alternar Navegação", + "Uncategorized": "Sem Categoria", + "Up": "Cima", + "Updates": "Actualizaçōes", "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", + "updated": "Actualizado", } diff --git a/nikola/data/themes/base/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py index b2877e4..f33abcc 100644 --- a/nikola/data/themes/base/messages/messages_pt_br.py +++ b/nikola/data/themes/base/messages/messages_pt_br.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d mín restante para leitura", "(active)": "(ativo)", "Also available in:": "Também disponível em:", "Archive": "Arquivo", + "Atom feed": "Feed Atom", "Authors": "Autores", "Categories": "Categorias", "Comments": "Comentários", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Mais posts sobre %s", "Newer posts": "Posts mais recentes", "Next post": "Próximo post", + "Next": "Próximo", "No posts found.": "Nenhum tópico encontrado.", "Nothing found.": "Nada encontrado.", "Older posts": "Posts mais antigos", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Posts sobre %s", "Posts by %s": "Postado por %s", "Posts for year %s": "Posts do ano %s", - "Posts for {month} {day}, {year}": "Posts do {day} {month}, {year}", - "Posts for {month} {year}": "Posts de {month} {year}", + "Posts for {month_day_year}": "Posts do {month_day_year}", + "Posts for {month_year}": "Posts de {month_year}", "Previous post": "Post anterior", + "Previous": "Anterior", "Publication date": "Data de publicação", "RSS feed": "Feed RSS", "Read in English": "Ler em português", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Tags", "Toggle navigation": "Alternar navegação", "Uncategorized": "Sem categoria", + "Up": "acima", "Updates": "Atualizações", "Write your page here.": "Insira a sua página aqui", "Write your post here.": "Escreva o seu comentário aqui.", "old posts, page %d": "Posts antigos, página %d", "page %d": "página %d", + "updated": "Atualizado", } diff --git a/nikola/data/themes/base/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py index 0070b94..f2f33cc 100644 --- a/nikola/data/themes/base/messages/messages_ru.py +++ b/nikola/data/themes/base/messages/messages_ru.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d минут чтения осталось", "(active)": "(активная)", "Also available in:": "Также доступно на:", "Archive": "Архив", + "Atom feed": "", "Authors": "Разработчики", "Categories": "Категории", "Comments": "Комментарии", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Больше записей о %s", "Newer posts": "Новые записи", "Next post": "Следующая запись", + "Next": "Следующая", "No posts found.": "Записей не найдено.", "Nothing found.": "Ничего не найдено.", "Older posts": "Старые записи", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Записи о %s", "Posts by %s": "Запись %s", "Posts for year %s": "Записи за %s год", - "Posts for {month} {day}, {year}": "Записи за {day} {month} {year}", - "Posts for {month} {year}": "Записи за {month} {year}", + "Posts for {month_day_year}": "Записи за {month_day_year}", + "Posts for {month_year}": "Записи за {month_year}", "Previous post": "Предыдущая запись", + "Previous": "Предыдущая", "Publication date": "Дата опубликования", "RSS feed": "RSS лента", "Read in English": "Прочесть по-русски", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Подкатегории:", "Tags and Categories": "Тэги и категории", "Tags": "Тэги", - "Toggle navigation": "", + "Toggle navigation": "Включить навигацию", "Uncategorized": "Несортированное", + "Up": "Наверх", "Updates": "Обновления", "Write your page here.": "Создайте Вашу страницу здесь.", "Write your post here.": "Создайте Вашу запись здесь.", "old posts, page %d": "%d страница со старыми записями", "page %d": "%d страница", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_si_lk.py b/nikola/data/themes/base/messages/messages_si_lk.py deleted file mode 100644 index 6107c54..0000000 --- a/nikola/data/themes/base/messages/messages_si_lk.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- encoding:utf-8 -*- -from __future__ import unicode_literals - -MESSAGES = { - "%d min remaining to read": "", - "(active)": "", - "Also available in:": "", - "Archive": "", - "Authors": "", - "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 by %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": "", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", - "Write your page here.": "", - "Write your post here.": "", - "old posts, page %d": "", - "page %d": "", -} diff --git a/nikola/data/themes/base/messages/messages_sk.py b/nikola/data/themes/base/messages/messages_sk.py index 7b7df6f..aea46f2 100644 --- a/nikola/data/themes/base/messages/messages_sk.py +++ b/nikola/data/themes/base/messages/messages_sk.py @@ -1,12 +1,13 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "zostáva %d minút na čítanie", "(active)": "(aktívne)", "Also available in:": "Tiež dostupné v:", "Archive": "Archív", - "Authors": "", + "Atom feed": "Atom kanál", + "Authors": "Autori", "Categories": "Kategórie", "Comments": "Komentáre", "LANGUAGE": "Slovenčina", @@ -14,17 +15,19 @@ MESSAGES = { "More posts about %s": "Viac príspevkov o %s", "Newer posts": "Novšie príspevky", "Next post": "Nasledujúci príspevok", + "Next": "Nasledujúci", "No posts found.": "Žiadne príspevky nenájdené", "Nothing found.": "Nič nenájdené.", "Older posts": "Staršie príspevky", "Original site": "Pôvodná stránka", "Posted:": "Zverejnené:", "Posts about %s": "Príspevky o %s", - "Posts by %s": "", + "Posts by %s": "Príspevky od %s", "Posts for year %s": "Príspevky z roku %s", - "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}", + "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", + "Previous": "Predchádzajúci", "Publication date": "Dátum zverejnenia", "RSS feed": "RSS kanál", "Read in English": "Čítať v slovenčine", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Podkategórie:", "Tags and Categories": "Štítky a kategórie", "Tags": "Štítky", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", + "Toggle navigation": "Prepnúť navigáciu", + "Uncategorized": "Nekategorizované", + "Up": "Dohora", + "Updates": "Aktualizácie", "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", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py index 31f3a58..6bd6370 100644 --- a/nikola/data/themes/base/messages/messages_sl.py +++ b/nikola/data/themes/base/messages/messages_sl.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "še %d min za branje preostanka", "(active)": "", "Also available in:": "Na voljo tudi v:", "Archive": "Arhiv", + "Atom feed": "", "Authors": "", "Categories": "Kategorije", "Comments": "Komentarji", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Več objav o %s", "Newer posts": "Novejše objave", "Next post": "Naslednja objava", + "Next": "", "No posts found.": "Ni najdenih objav.", "Nothing found.": "Brez zadetkov.", "Older posts": "Starejše objave", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Objave o %s", "Posts by %s": "", "Posts for year %s": "Objave za leto %s", - "Posts for {month} {day}, {year}": "Objave za {day}. {month}, {year}", - "Posts for {month} {year}": "Objave za {month} {year}", + "Posts for {month_day_year}": "Objave za {month_day_year}", + "Posts for {month_year}": "Objave za {month_year}", "Previous post": "Prejšnja objava", + "Previous": "", "Publication date": "Datum objave", "RSS feed": "vir RSS", "Read in English": "Beri v slovenščini", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Značke", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "", "Write your post here.": "", "old posts, page %d": "stare objave, stran %d", "page %d": "stran %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_sq.py b/nikola/data/themes/base/messages/messages_sq.py index 6d5e39a..088cdcb 100644 --- a/nikola/data/themes/base/messages/messages_sq.py +++ b/nikola/data/themes/base/messages/messages_sq.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d min ngelen për tu lexuar", "(active)": "(aktiv)", "Also available in:": "Gjithashtu e disponueshme në:", "Archive": "Arkiva", + "Atom feed": "", "Authors": "Autorë", "Categories": "Kategori", "Comments": "Komente", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Më shumë postime rreth %s", "Newer posts": "Postime më të reja", "Next post": "Postimi i rradhës", + "Next": "", "No posts found.": "Nuk është gjetur asnjë post.", "Nothing found.": "Nuk është gjetur asgjë.", "Older posts": "Postime më të vjetra", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Postime rreth %s", "Posts by %s": "Postime nga %s", "Posts for year %s": "Postime për vitin %s", - "Posts for {month} {day}, {year}": "Postime për {month} {day}, {year}", - "Posts for {month} {year}": "Postime për {month} {year}", + "Posts for {month_day_year}": "Postime për {month_day_year}", + "Posts for {month_year}": "Postime për {month_year}", "Previous post": "Postim i kaluar", + "Previous": "", "Publication date": "Data e publikimit", "RSS feed": "Furnizim RSS", "Read in English": "Lexo në Shqip", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etiketa", "Toggle navigation": "", "Uncategorized": "E pa kategorizuar", + "Up": "", "Updates": "Përditësime", "Write your page here.": "Shkruaj faqen tënde këtu.", "Write your post here.": "Shkruaj postin tënd këtu.", "old posts, page %d": "Postime të kaluara, faqe %d", "page %d": "faqe %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_sr.py b/nikola/data/themes/base/messages/messages_sr.py index 03f1024..98e023e 100644 --- a/nikola/data/themes/base/messages/messages_sr.py +++ b/nikola/data/themes/base/messages/messages_sr.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d минута је преостало за читање", "(active)": "(активно)", "Also available in:": "Такође доступан у:", "Archive": "Архива", + "Atom feed": "", "Authors": "", "Categories": "Категорије", "Comments": "Коментари", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Више постова о %s", "Newer posts": "Новији постови", "Next post": "Следећи пост", + "Next": "", "No posts found.": "Нема постова.", "Nothing found.": "Није ништа пронађено.", "Older posts": "Старији постови", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Постови о %s", "Posts by %s": "", "Posts for year %s": "Постови за годину %s", - "Posts for {month} {day}, {year}": "Објаве за {month} {day}, {year}", - "Posts for {month} {year}": "Постови за {month} {year}", + "Posts for {month_day_year}": "Објаве за {month_day_year}", + "Posts for {month_year}": "Постови за {month_year}", "Previous post": "Претходни пост", + "Previous": "", "Publication date": "Датум објаве", "RSS feed": "RSS feed", "Read in English": "Прочитај на српском", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Тагови", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "Write your page here.": "Вашу страницу напишите овдје.", "Write your post here.": "Вашу објаву напишите овдје.", "old posts, page %d": "стари постови, страна %d", "page %d": "страна %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_sr_latin.py b/nikola/data/themes/base/messages/messages_sr_latin.py index f7f3727..4b1e827 100644 --- a/nikola/data/themes/base/messages/messages_sr_latin.py +++ b/nikola/data/themes/base/messages/messages_sr_latin.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minuta preostalo za čitanje", "(active)": "(aktivno)", "Also available in:": "Takođe dostupan u:", "Archive": "Arhiva", + "Atom feed": "", "Authors": "", "Categories": "Kategorije", "Comments": "Komentari", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Više objava o %s", "Newer posts": "Novije objave", "Next post": "Naredna objava", + "Next": "", "No posts found.": "Nema objava.", "Nothing found.": "Ništa nije pronađeno.", "Older posts": "Starije objave", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Objave o %s", "Posts by %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}", + "Posts for {month_day_year}": "Objave za {month_day_year}", + "Posts for {month_year}": "Objave za {month_year}", "Previous post": "Prethodne objave", + "Previous": "", "Publication date": "Datum objavljivanja", "RSS feed": "RSS feed", "Read in English": "Pročitaj na bosanskom", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Oznake", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "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", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_sv.py b/nikola/data/themes/base/messages/messages_sv.py index 106138f..ce56b11 100644 --- a/nikola/data/themes/base/messages/messages_sv.py +++ b/nikola/data/themes/base/messages/messages_sv.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d minuter kvar att läsa", "(active)": "(aktiv)", "Also available in:": "Även tillgänglig på:", "Archive": "Arkiv", + "Atom feed": "", "Authors": "", "Categories": "Kategorier", "Comments": "Kommentarer", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "Fler inlägg om %s", "Newer posts": "Nya inlägg", "Next post": "Nästa inlägg", + "Next": "", "No posts found.": "Inga inlägg hittade.", "Nothing found.": "Inget hittat.", "Older posts": "Äldre inlägg", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "Inlägg om %s", "Posts by %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}", + "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", + "Previous": "", "Publication date": "Publiceringsdatum", "RSS feed": "RSS-flöde", "Read in English": "Läs på svenska", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Taggar", "Toggle navigation": "", "Uncategorized": "", + "Up": "", "Updates": "", "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", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_te.py b/nikola/data/themes/base/messages/messages_te.py index 70d8150..385ff0b 100644 --- a/nikola/data/themes/base/messages/messages_te.py +++ b/nikola/data/themes/base/messages/messages_te.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d నిమిషాలు చదవడానికి కావలెను ", "(active)": "(క్రియాశీల)", "Also available in:": "ఇందులో కూడా లభించును:", "Archive": "అభిలేఖలు ", + "Atom feed": "అటామ్ ఫీడ్", "Authors": "రచయితలు", "Categories": "వర్గాలు", "Comments": "వ్యాఖ్యలు", - "LANGUAGE": "ఆంగ్లం ", + "LANGUAGE": "తెలుగు", "Languages:": "భాషలు:", "More posts about %s": "%s గూర్చి మరిన్ని టపాలు", "Newer posts": "కొత్త టపాలు", - "Next post": "తరువాత టపా", - "No posts found.": "", - "Nothing found.": "", + "Next post": "తరువాతి టపా", + "Next": "తరువాతి", + "No posts found.": "టపాలు ఏవీ కనుగొనబడలేదు.", + "Nothing found.": "ఏదీ కనుగొనబడలేదు.", "Older posts": "పాత టపాలు", "Original site": "వాస్తవ సైట్", "Posted:": "ప్రచురుంచిన తేదీ:", "Posts about %s": "%s గూర్చి టపాలు", "Posts by %s": "%s యొక్క టపాలు", "Posts for year %s": "%s సంవత్సర టపాలు", - "Posts for {month} {day}, {year}": "", - "Posts for {month} {year}": "", + "Posts for {month_day_year}": "{month_day_year} యొక్క టపాలు", + "Posts for {month_year}": "{month_year} యొక్క టపాలు", "Previous post": "మునుపటి టపా", + "Previous": "మునుపటి", "Publication date": "ప్రచురణ తేదీ", "RSS feed": "RSS ఫీడ్", - "Read in English": "ఆంగ్లంలో చదవండి", + "Read in English": "తెలుగులో చదవండి", "Read more": "ఇంకా చదవండి", "Skip to main content": "ప్రధాన విషయానికి వెళ్ళు", "Source": "మూలం", "Subcategories:": "ఉపవర్గాలు:", "Tags and Categories": "ట్యాగ్లు మరియు వర్గాలు", "Tags": "ట్యాగ్లు", - "Toggle navigation": "", + "Toggle navigation": "నావిగేషన్ను టోగుల్ చేయండి", "Uncategorized": "వర్గీకరించని", + "Up": "పైకి", "Updates": "నవీకరణలు", "Write your page here.": "మీ పేజీ ఇక్కడ రాయండి.", "Write your post here.": "ఇక్కడ మీ టపా ను వ్రాయండి.", "old posts, page %d": "పాత టపాలు, పేజీ %d", "page %d": "పేజీ %d", + "updated": "నవీకరించబడింది", } diff --git a/nikola/data/themes/base/messages/messages_th.py b/nikola/data/themes/base/messages/messages_th.py new file mode 100644 index 0000000..a46f9ca --- /dev/null +++ b/nikola/data/themes/base/messages/messages_th.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "ใช้เวลาอ่านอีก %d นาที", + "(active)": "(ถูกใช้งาน)", + "Also available in:": "ภาษาอื่นๆ:", + "Archive": "คลังโพสต์", + "Atom feed": "ฟีด Atom", + "Authors": "ผู้เขียน", + "Categories": "หมวดหมู่", + "Comments": "ความคิดเห็น", + "LANGUAGE": "ภาษาไทย", + "Languages:": "ภาษา:", + "More posts about %s": "โพสต์เพิ่มเติมเกี่ยวกับ %s", + "Newer posts": "โพสต์มาใหม่", + "Next post": "โพสต์ถัดไป", + "Next": "ถัดไป", + "No posts found.": "ไม่พบโพสต์.", + "Nothing found.": "ไม่พบ", + "Older posts": "โพสต์เก่า", + "Original site": "เว็บไซต์ที่มา", + "Posted:": "โพสต์เมื่อ:", + "Posts about %s": "โพสต์เกี่ยวกับ %s", + "Posts by %s": "โพสต์โดย %s", + "Posts for year %s": "โพสต์เมื่อปี %s", + "Posts for {month_day_year}": "โพสต์เมื่อ {month_day_year}", + "Posts for {month_year}": "โพสต์เมื่อ {month_year}", + "Previous post": "โพสต์ก่อนหน้า", + "Previous": "ก่อนหน้า", + "Publication date": "วันที่ตีพิมพ์", + "RSS feed": "ฟีด RSS", + "Read in English": "อ่านเป็นภาษาไทย", + "Read more": "อ่านเพิ่มเติม", + "Skip to main content": "ข้ามไปหน้าหลัก", + "Source": "แหล่งที่มา", + "Subcategories:": "หมวดหมู่ย่อย:", + "Tags and Categories": "แท็กและหมวดหมู่", + "Tags": "แท็ก", + "Toggle navigation": "เมนูบาร์", + "Uncategorized": "ไม่มีหมวดหมู่", + "Up": "ขึ้นด้านบน", + "Updates": "อัพเดด", + "Write your page here.": "เขียนเพจที่นี่", + "Write your post here.": "เขียนโพสต์ที่นี่", + "old posts, page %d": "โพสต์เก่า, หน้า %d", + "page %d": "หน้า %d", + "updated": "", +} diff --git a/nikola/data/themes/base/messages/messages_tl.py b/nikola/data/themes/base/messages/messages_tl.py deleted file mode 100644 index 1b85eb4..0000000 --- a/nikola/data/themes/base/messages/messages_tl.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- encoding:utf-8 -*- -from __future__ import unicode_literals - -MESSAGES = { - "%d min remaining to read": "", - "(active)": "", - "Also available in:": "", - "Archive": "", - "Authors": "", - "Categories": "", - "Comments": "", - "LANGUAGE": "Ingles", - "Languages:": "Mga Wika:", - "More posts about %s": "", - "Newer posts": "", - "Next post": "Susunod", - "No posts found.": "", - "Nothing found.": "", - "Older posts": "", - "Original site": "", - "Posted:": "", - "Posts about %s": "", - "Posts by %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": "Mga Tag", - "Toggle navigation": "", - "Uncategorized": "", - "Updates": "", - "Write your page here.": "", - "Write your post here.": "", - "old posts, page %d": "", - "page %d": "", -} diff --git a/nikola/data/themes/base/messages/messages_tr.py b/nikola/data/themes/base/messages/messages_tr.py index e09dcb8..8249f42 100644 --- a/nikola/data/themes/base/messages/messages_tr.py +++ b/nikola/data/themes/base/messages/messages_tr.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d dakikalık okuma", "(active)": "(etkin)", "Also available in:": "Şu dilde de mevcut:", "Archive": "Arşiv", + "Atom feed": "", "Authors": "Yazarlar", "Categories": "Kategoriler", "Comments": "Yorumlar", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "%s hakkında diğer yazılar", "Newer posts": "Daha yeni yazılar", "Next post": "Sonraki yazı", + "Next": "", "No posts found.": "Yazı bulunamadı.", "Nothing found.": "Hiçbir şey bulunamadı.", "Older posts": "Daha eski yazılar", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "%s ile ilgili yazılar", "Posts by %s": "%s tarafından yazılanlar", "Posts for year %s": "%s yılındaki yazılar", - "Posts for {month} {day}, {year}": "{month} {day}, {year} tarihinden itibaren yazılar", - "Posts for {month} {year}": "{month} {year} göre yazılar", + "Posts for {month_day_year}": "{month_day_year} tarihinden itibaren yazılar", + "Posts for {month_year}": "{month_year} göre yazılar", "Previous post": "Önceki yazı", + "Previous": "", "Publication date": "Yayınlanma tarihi", "RSS feed": "RSS kaynağı", "Read in English": "Türkçe olarak oku", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "Etiketler", "Toggle navigation": "", "Uncategorized": "Kategorisiz", + "Up": "", "Updates": "Güncellemeler", "Write your page here.": "Sayfanızı buraya yazın.", "Write your post here.": "Yazınızı buraya yazın.", "old posts, page %d": "eski yazılar, sayfa %d", "page %d": "sayfa %d", + "updated": "", } diff --git a/nikola/data/themes/base/messages/messages_uk.py b/nikola/data/themes/base/messages/messages_uk.py index 327a5d9..2a6d84f 100644 --- a/nikola/data/themes/base/messages/messages_uk.py +++ b/nikola/data/themes/base/messages/messages_uk.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "Залишилось читати %d хвилин", "(active)": "(активне)", "Also available in:": "Іншою мовою:", "Archive": "Архів", + "Atom feed": "Стрічка у форматі Atom", "Authors": "Автори", "Categories": "Категорії", "Comments": "Коментарі", @@ -14,19 +15,21 @@ MESSAGES = { "More posts about %s": "Більше статей про %s", "Newer posts": "Новіші статті", "Next post": "Наступна стаття", + "Next": "Вперед", "No posts found.": "Не знайдено жодної статті", "Nothing found.": "Нічого не знайдено", - "Older posts": "Більш старі статті", + "Older posts": "Старіші статті", "Original site": "Оригінал сайту", "Posted:": "Опублікована:", "Posts about %s": "Статті про %s", "Posts by %s": "Статті %s", "Posts for year %s": "Статті за %s рік", - "Posts for {month} {day}, {year}": "Статті за {month} {day}, {year}", - "Posts for {month} {year}": "Статті за {month} {year}", + "Posts for {month_day_year}": "Статті за {month_day_year}", + "Posts for {month_year}": "Статті за {month_year}", "Previous post": "Попередня стаття", + "Previous": "Назад", "Publication date": "Дата публікації", - "RSS feed": "RSS-стрічка", + "RSS feed": "Стрічка у форматі RSS", "Read in English": "Читати українською", "Read more": "Читати далі", "Skip to main content": "Перейти до основного матеріалу", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "Підкатегорії:", "Tags and Categories": "Теги і категорії", "Tags": "Теги", - "Toggle navigation": "", + "Toggle navigation": "Перемкнути навігацію", "Uncategorized": "Без категорії", + "Up": "Нагору", "Updates": "Підписки", "Write your page here.": "Напишіть Вашу сторінку тут.", "Write your post here.": "Напишить Вашу статтю тут.", "old posts, page %d": "старі статті, сторінка %d", "page %d": "сторінка %d", + "updated": "оновлено", } diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py index 055f72e..2e6cd49 100644 --- a/nikola/data/themes/base/messages/messages_ur.py +++ b/nikola/data/themes/base/messages/messages_ur.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "%d منٹ کا مطالعہ باقی", "(active)": "(فعال)", "Also available in:": "ان میں بھی دستیاب:", "Archive": "آرکائیو", + "Atom feed": "ایٹم فِیڈ", "Authors": "مصنفین", "Categories": "زمرے", "Comments": "تبصرے", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "%s کے بارے میں مزید تحاریر", "Newer posts": "نئی تحاریر", "Next post": "اگلی تحریر", + "Next": "آگے", "No posts found.": "کوئی تحریر نہیں مل سکی۔", "Nothing found.": "کچھ نہیں مل سکا۔", "Older posts": "پرانی تحاریر", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "%s کے بارے میں تحاریر", "Posts by %s": "%s کی تحاریر", "Posts for year %s": "سال %s کی تحاریر", - "Posts for {month} {day}, {year}": "{day} {month}، {year} کی تحاریر", - "Posts for {month} {year}": "{month} {year} کی تحاریر", + "Posts for {month_day_year}": "{month_day_year} کی تحاریر", + "Posts for {month_year}": "{month_year} کی تحاریر", "Previous post": "پچھلی تحریر", + "Previous": "پیچھے", "Publication date": "تاریخِ اشاعت", "RSS feed": "آر ایس ایس فیڈ", "Read in English": "اردو میں پڑھیں", @@ -34,11 +37,13 @@ MESSAGES = { "Subcategories:": "ذیلی زمرے", "Tags and Categories": "ٹیگز اور زمرے", "Tags": "ٹیگز", - "Toggle navigation": "", + "Toggle navigation": "نیویگیشن ہٹائیں/دکھائیں", "Uncategorized": "بے زمرہ", + "Up": "اوپر", "Updates": "تازہ ترین", "Write your page here.": "اپنے صفحے کا متن یہاں لکھیں۔", "Write your post here.": "اپنی تحریر یہاں لکھیں۔", "old posts, page %d": "پرانی تحاریر صفحہ %d", "page %d": "صفحہ %d", + "updated": "تازہ کاری", } diff --git a/nikola/data/themes/base/messages/messages_vi.py b/nikola/data/themes/base/messages/messages_vi.py new file mode 100644 index 0000000..5ec8bbe --- /dev/null +++ b/nikola/data/themes/base/messages/messages_vi.py @@ -0,0 +1,49 @@ +# -*- encoding:utf-8 -*- +"""Autogenerated file, do not edit. Submit translations on Transifex.""" + +MESSAGES = { + "%d min remaining to read": "Cần %d phút để đọc", + "(active)": "(active)", + "Also available in:": "Cũng có sẵn trong:", + "Archive": "Kho", + "Atom feed": "Nguồn cung cấp dữ liệu Atom", + "Authors": "Tác giả", + "Categories": "Thể loại", + "Comments": "Bình luận", + "LANGUAGE": "Tiếng Việt", + "Languages:": "Ngôn ngữ:", + "More posts about %s": "Các bài đăng khác về %s", + "Newer posts": "Bài viết gần đây", + "Next post": "Bài viết tiếp theo", + "Next": "Kế tiếp", + "No posts found.": "Không tìm thấy bài viết.", + "Nothing found.": "Không có kết quả.", + "Older posts": "Bài viết trước đây", + "Original site": "Trang gốc", + "Posted:": "Đã đăng:", + "Posts about %s": "Bài viết về %s", + "Posts by %s": "Bài đăng bởi %s", + "Posts for year %s": "Các bài viết trong năm %s", + "Posts for {month_day_year}": "Các bài đã đăng {month_day_year}", + "Posts for {month_year}": "Các bài đã đăng {month_year}", + "Previous post": "Bài viết trước", + "Previous": "Trước", + "Publication date": "Ngày phát hành", + "RSS feed": "Nguồn cung cấp dữ liệu RSS", + "Read in English": "Phiên bản Tiếng Việt", + "Read more": "Đọc thêm", + "Skip to main content": "Chuyển đến nội dung chính", + "Source": "Nguồn", + "Subcategories:": "Thể loại con:", + "Tags and Categories": "Thẻ và Thể loại", + "Tags": "Thẻ", + "Toggle navigation": "Chuyển điều hướng", + "Uncategorized": "Chưa được phân loại", + "Up": "Trở lên", + "Updates": "Cập nhật", + "Write your page here.": "Bắt đầu viết nội dung của trang ở đây.", + "Write your post here.": "Bắt đầu viết bài ở đây.", + "old posts, page %d": "các bài viết trước đây, trang %d", + "page %d": "trang %d", + "updated": "Đã được cập nhật", +} diff --git a/nikola/data/themes/base/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py index 84e4317..df8570b 100644 --- a/nikola/data/themes/base/messages/messages_zh_cn.py +++ b/nikola/data/themes/base/messages/messages_zh_cn.py @@ -1,44 +1,49 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { - "%d min remaining to read": "剩余 %d 分钟去阅读", + "%d min remaining to read": "剩余阅读时间 %d 分钟", "(active)": "(活跃)", - "Also available in:": "其他语言版本:", - "Archive": "文章存档", + "Also available in:": "也可用于:", + "Archive": "文章归档", + "Atom feed": "Atom 源", "Authors": "作者", "Categories": "分类", - "Comments": "注释", + "Comments": "评论", "LANGUAGE": "简体中文", "Languages:": "语言:", - "More posts about %s": "更多 %s 相关文章", - "Newer posts": "新一篇", - "Next post": "后一篇", + "More posts about %s": "关于%s 的更多文章 ", + "Newer posts": "较新的文章", + "Next post": "下一篇文章", + "Next": "项下", "No posts found.": "没有找到文章", "Nothing found.": "没有找到。", - "Older posts": "旧一篇", + "Older posts": "以前的文章", "Original site": "原文地址", "Posted:": "发表于:", - "Posts about %s": "文章分类:%s", - "Posts by %s": "%s 发布", + "Posts about %s": "关于文章 %s", + "Posts by %s": "文章有%s发布", "Posts for year %s": "%s年文章", - "Posts for {month} {day}, {year}": "{year}年{month}月{day}日文章", - "Posts for {month} {year}": "{year}年{month}月文章", - "Previous post": "前一篇", + "Posts for {month_day_year}": "{month_day_year}文章", + "Posts for {month_year}": "{month_year}文章", + "Previous post": "上一篇文章", + "Previous": "以前", "Publication date": "发布日期", "RSS feed": "RSS 源", - "Read in English": "中文版", - "Read more": "更多", + "Read in English": "阅读简体中文", + "Read more": "阅读更多", "Skip to main content": "跳到主内容", - "Source": "源代码", + "Source": "源文件", "Subcategories:": "子类别:", "Tags and Categories": "标签和分类", "Tags": "标签", - "Toggle navigation": "", + "Toggle navigation": "展开导航栏", "Uncategorized": "未分类", + "Up": "向上", "Updates": "更新", "Write your page here.": "在这里书写你的页面。", "Write your post here.": "在这里书写你的文章。", "old posts, page %d": "旧文章页 %d", "page %d": "第 %d 页", + "updated": "更新", } diff --git a/nikola/data/themes/base/messages/messages_zh_tw.py b/nikola/data/themes/base/messages/messages_zh_tw.py index e6de0ff..ee4befd 100644 --- a/nikola/data/themes/base/messages/messages_zh_tw.py +++ b/nikola/data/themes/base/messages/messages_zh_tw.py @@ -1,11 +1,12 @@ # -*- encoding:utf-8 -*- -from __future__ import unicode_literals +"""Autogenerated file, do not edit. Submit translations on Transifex.""" MESSAGES = { "%d min remaining to read": "尚餘 %d 分鐘", "(active)": "(啟用)", "Also available in:": "其他語言版本:", "Archive": "彙整", + "Atom feed": "", "Authors": "作者", "Categories": "分類", "Comments": "迴響", @@ -14,6 +15,7 @@ MESSAGES = { "More posts about %s": "更多 %s 的文章", "Newer posts": "較新的文章", "Next post": "下一篇", + "Next": "下一篇", "No posts found.": "沒有找到文章。", "Nothing found.": "沒有找到。", "Older posts": "較舊的文章", @@ -22,9 +24,10 @@ MESSAGES = { "Posts about %s": "文章分類:%s", "Posts by %s": "%s 發佈", "Posts for year %s": "%s 年的文章", - "Posts for {month} {day}, {year}": "{year}年{month}月{day}日的文章", - "Posts for {month} {year}": "{year}年{month}月的文章", + "Posts for {month_day_year}": "{month_day_year}的文章", + "Posts for {month_year}": "{month_year}的文章", "Previous post": "上一篇", + "Previous": "上一篇", "Publication date": "發佈日期", "RSS feed": "RSS 訂閱", "Read in English": "繁體中文版", @@ -36,9 +39,11 @@ MESSAGES = { "Tags": "標籤", "Toggle navigation": "切換導航", "Uncategorized": "未分類", + "Up": "向上", "Updates": "更新", "Write your page here.": "從這裡開始編輯頁面", "Write your post here.": "從這裡開始編輯文章", "old posts, page %d": "舊文章,第 %d 頁", "page %d": "第 %d 頁", + "updated": "", } diff --git a/nikola/data/themes/base/templates/archive.tmpl b/nikola/data/themes/base/templates/archive.tmpl new file mode 100644 index 0000000..d6f107c --- /dev/null +++ b/nikola/data/themes/base/templates/archive.tmpl @@ -0,0 +1 @@ +<%inherit file="list_post.tmpl"/> diff --git a/nikola/data/themes/base/templates/archive_navigation_helper.tmpl b/nikola/data/themes/base/templates/archive_navigation_helper.tmpl new file mode 100644 index 0000000..506629f --- /dev/null +++ b/nikola/data/themes/base/templates/archive_navigation_helper.tmpl @@ -0,0 +1,27 @@ +## -*- coding: utf-8 -*- + +<%def name="archive_navigation()"> +%if 'archive_page' in pagekind: + %if has_archive_navigation: + <nav class="archivenav"> + <ul class="pager"> + %if previous_archive: + <li class="previous"><a href="${previous_archive}" rel="prev">${messages("Previous")}</a></li> + %else: + <li class="previous disabled"><a href="#" rel="prev">${messages("Previous")}</a></li> + % endif + %if up_archive: + <li class="up"><a href="${up_archive}" rel="up">${messages("Up")}</a></li> + %else: + <li class="up disabled"><a href="#" rel="up">${messages("Up")}</a></li> + %endif + %if next_archive: + <li class="next"><a href="${next_archive}" rel="next">${messages("Next")}</a></li> + %else: + <li class="next disabled"><a href="#" rel="next">${messages("Next")}</a></li> + %endif + </ul> + </nav> + %endif +% endif +</%def> diff --git a/nikola/data/themes/base/templates/archiveindex.tmpl b/nikola/data/themes/base/templates/archiveindex.tmpl index 8c58f13..eb4dd27 100644 --- a/nikola/data/themes/base/templates/archiveindex.tmpl +++ b/nikola/data/themes/base/templates/archiveindex.tmpl @@ -1,13 +1,20 @@ ## -*- coding: utf-8 -*- <%inherit file="index.tmpl"/> +<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%block name="extra_head"> ${parent.extra_head()} - %if len(translations) > 1 and generate_atom: - %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: - <link rel="alternate" type="application/atom+xml" title="Atom for the ${archive_name} archive" href="${_link("archive_atom", archive_name)}"> - %endif + ${feeds_translations.head(archive_name, kind, rss_override=False)} +</%block> + +<%block name="content_header"> + <header> + <h1>${title|h}</h1> + ${archive_nav.archive_navigation()} + <div class="metadata"> + ${feeds_translations.feed_link(archive, kind)} + ${feeds_translations.translation_link(kind)} + </div> + </header> </%block> diff --git a/nikola/data/themes/base/templates/author.tmpl b/nikola/data/themes/base/templates/author.tmpl index 21d8d64..983f118 100644 --- a/nikola/data/themes/base/templates/author.tmpl +++ b/nikola/data/themes/base/templates/author.tmpl @@ -1,43 +1,28 @@ ## -*- coding: utf-8 -*- <%inherit file="list_post.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%block name="extra_head"> - ${parent.extra_head()} - %if len(translations) > 1 and generate_rss: - %for language in sorted(translations): - <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${author|h} (${language})" href="${_link(kind + "_rss", author, language)}"> - %endfor - %elif generate_rss: - <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${author|h}" href="${_link(kind + "_rss", author)}"> - %endif + ${feeds_translations.head(author, kind, rss_override=False)} </%block> - <%block name="content"> <article class="authorpage"> <header> <h1>${title|h}</h1> %if description: - <p>${description}</p> + <p>${description}</p> %endif <div class="metadata"> - %if len(translations) > 1 and generate_rss: - %for language in sorted(translations): - <p class="feedlink"> - <a href="${_link(kind + "_rss", author, language)}" hreflang="${language}" type="application/rss+xml">${messages('RSS feed', language)} (${language})</a> - </p> - %endfor - %elif generate_rss: - <p class="feedlink"><a href="${_link(kind + "_rss", author)}" type="application/rss+xml">${messages('RSS feed')}</a></p> - %endif + ${feeds_translations.feed_link(author, kind)} </div> </header> %if posts: - <ul class="postlist"> - % for post in posts: - <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}</a></li> - % endfor - </ul> + <ul class="postlist"> + % for post in posts: + <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}</a></li> + % endfor + </ul> %endif </article> </%block> diff --git a/nikola/data/themes/base/templates/authorindex.tmpl b/nikola/data/themes/base/templates/authorindex.tmpl index 34cb20b..fe9d39e 100644 --- a/nikola/data/themes/base/templates/authorindex.tmpl +++ b/nikola/data/themes/base/templates/authorindex.tmpl @@ -1,13 +1,21 @@ ## -*- coding: utf-8 -*- <%inherit file="index.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="content_header"> + <header> + <h1>${title|h}</h1> + %if description: + <p>${description}</p> + %endif + <div class="metadata"> + ${feeds_translations.feed_link(author, kind)} + ${feeds_translations.translation_link(kind)} + </div> + </header> +</%block> <%block name="extra_head"> ${parent.extra_head()} - %if len(tranlations) > 1 and generate_atom: - %for language in sorted(translations): - <link rel="alternate" type="application/atom+xml" title="Atom for the ${author|h} section (${language})" href="${_link(kind + "_atom", author, language)}"> - %endfor - %elif generate_atom: - <link rel="alternate" type="application/atom+xml" title="Atom for the ${author|h} section" href="${_link("author" + "_atom", author)}"> - %endif + ${feeds_translations.head(author, kind, rss_override=False)} </%block> diff --git a/nikola/data/themes/base/templates/authors.tmpl b/nikola/data/themes/base/templates/authors.tmpl index 141c560..8503bd4 100644 --- a/nikola/data/themes/base/templates/authors.tmpl +++ b/nikola/data/themes/base/templates/authors.tmpl @@ -1,10 +1,18 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="extra_head"> + ${feeds_translations.head(kind=kind, feeds=False)} +</%block> <%block name="content"> <article class="authorindex"> %if items: <h2>${messages("Authors")}</h2> + <div class="metadata"> + ${feeds_translations.translation_link(kind)} + </div> <ul class="postlist"> % for text, link in items: % if text not in hidden_authors: diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl index 2b0cbfd..f071c95 100644 --- a/nikola/data/themes/base/templates/base.tmpl +++ b/nikola/data/themes/base/templates/base.tmpl @@ -2,8 +2,8 @@ <%namespace name="base" file="base_helper.tmpl" import="*"/> <%namespace name="header" file="base_header.tmpl" import="*"/> <%namespace name="footer" file="base_footer.tmpl" import="*"/> -<%namespace name="annotations" file="annotation_helper.tmpl"/> ${set_locale(lang)} +### <html> tag is included by base.html_headstart ${base.html_headstart()} <%block name="extra_head"> ### Leave this block alone. @@ -11,16 +11,29 @@ ${base.html_headstart()} ${template_hooks['extra_head']()} </head> <body> -<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a> + <a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a> <div id="container"> - ${header.html_header()} - <main id="content"> + ${header.html_header()} + <main id="content"> <%block name="content"></%block> - </main> - ${footer.html_footer()} + </main> + ${footer.html_footer()} </div> ${base.late_load_js()} + % if date_fanciness != 0: + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "${luxon_locales[lang]}"; + fancydates(${date_fanciness}, ${luxon_date_format}); + </script> + <!-- end fancy dates --> + % endif <%block name="extra_js"></%block> + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> ${body_end} ${template_hooks['body_end']()} </body> diff --git a/nikola/data/themes/base/templates/base_footer.tmpl b/nikola/data/themes/base/templates/base_footer.tmpl index cd41d37..7e44c75 100644 --- a/nikola/data/themes/base/templates/base_footer.tmpl +++ b/nikola/data/themes/base/templates/base_footer.tmpl @@ -1,5 +1,4 @@ ## -*- coding: utf-8 -*- -<%namespace name="base" file="base_helper.tmpl" import="*"/> <%def name="html_footer()"> %if content_footer: diff --git a/nikola/data/themes/base/templates/base_header.tmpl b/nikola/data/themes/base/templates/base_header.tmpl index 2ffcfee..b45744a 100644 --- a/nikola/data/themes/base/templates/base_header.tmpl +++ b/nikola/data/themes/base/templates/base_header.tmpl @@ -16,7 +16,7 @@ </%def> <%def name="html_site_title()"> - <h1 id="brand"><a href="${abs_link(_link("root", None, lang))}" title="${blog_title|h}" rel="home"> + <h1 id="brand"><a href="${_link("root", None, lang)}" title="${blog_title|h}" rel="home"> %if logo_url: <img src="${logo_url}" alt="${blog_title|h}" id="logo"> %endif @@ -30,13 +30,22 @@ <%def name="html_navigation_links()"> <nav id="menu"> <ul> - %for url, text in navigation_links[lang]: + ${html_navigation_links_entries(navigation_links)} + ${html_navigation_links_entries(navigation_alt_links)} + ${template_hooks['menu']()} + ${template_hooks['menu_alt']()} + </ul> + </nav> +</%def> + +<%def name="html_navigation_links_entries(navigation_links_source)"> + %for url, text in navigation_links_source[lang]: % if isinstance(url, tuple): <li> ${text} <ul> %for suburl, text in url: % if rel_link(permalink, suburl) == "#": - <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a></li> + <li class="active"><a href="${permalink}">${text}<span class="sr-only"> ${messages("(active)", lang)}</span></a></li> %else: <li><a href="${suburl}">${text}</a></li> %endif @@ -44,16 +53,12 @@ </ul> % else: % if rel_link(permalink, url) == "#": - <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a></li> + <li class="active"><a href="${permalink}">${text}<span class="sr-only"> ${messages("(active)", lang)}</span></a></li> %else: <li><a href="${url}">${text}</a></li> %endif % endif %endfor - ${template_hooks['menu']()} - ${template_hooks['menu_alt']()} - </ul> - </nav> </%def> <%def name="html_translation_header()"> diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl index e2ffab2..18801ed 100644 --- a/nikola/data/themes/base/templates/base_helper.tmpl +++ b/nikola/data/themes/base/templates/base_helper.tmpl @@ -1,31 +1,25 @@ ## -*- coding: utf-8 -*- +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%def name="html_headstart()"> <!DOCTYPE html> <html \ -prefix='\ -%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']): -og: http://ogp.me/ns# article: http://ogp.me/ns/article# \ -%endif -%if comment_system == 'facebook': -fb: http://ogp.me/ns/fb# -%endif -' \ -%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']): -vocab="http://ogp.me/ns" \ -%endif + prefix='\ + og: http://ogp.me/ns# article: http://ogp.me/ns/article# \ + %if comment_system == 'facebook': + fb: http://ogp.me/ns/fb# \ + %endif + ' \ + vocab="http://ogp.me/ns" \ % if is_rtl: -dir="rtl" \ + dir="rtl" \ % endif \ lang="${lang}"> <head> <meta charset="utf-8"> - % if use_base_tag: - <base href="${abs_link(permalink)}"> - % endif %if description: - <meta name="description" content="${description|h}"> + <meta name="description" content="${description|h}"> %endif <meta name="viewport" content="width=device-width"> %if title == blog_title: @@ -35,8 +29,11 @@ lang="${lang}"> %endif ${html_stylesheets()} - <meta content="${theme_color}" name="theme-color"> - ${html_feedlinks()} + <meta name="theme-color" content="${theme_color}"> + % if meta_generator_tag: + <meta name="generator" content="Nikola (getnikola.com)"> + % endif + ${feeds_translations.head(classification=None, kind='index', other=False)} <link rel="canonical" href="${abs_link(permalink)}"> %if favicons: @@ -56,29 +53,58 @@ lang="${lang}"> <link rel="next" href="${nextlink}" type="text/html"> %endif - ${mathjax_config} %if use_cdn: - <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + <!--[if lt IE 9]><script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script><![endif]--> %else: - <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang)}"></script><![endif]--> + <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5shiv-printshiv.min.js', lang, url_type)}"></script><![endif]--> %endif ${extra_head_data} </%def> <%def name="late_load_js()"> + % if use_bundles: + % if use_cdn: + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + <script src="/assets/js/all.js"></script> + % else: + <script src="/assets/js/all-nocdn.js"></script> + % endif + % else: + % if use_cdn: + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + % else: + <script src="/assets/js/baguetteBox.min.js"></script> + % endif + % endif + % if date_fanciness != 0: + % if date_fanciness == 2: + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script> + % endif + % if use_cdn: + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + % else: + <script src="/assets/js/luxon.min.js"></script> + % endif + % if not use_bundles: + <script src="/assets/js/fancydates.min.js"></script> + % endif + % endif ${social_buttons_code} </%def> <%def name="html_stylesheets()"> %if use_bundles: %if use_cdn: + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> %else: <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> %endif %else: - <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst_base.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/nikola_rst.css" rel="stylesheet" type="text/css"> <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"> %if has_custom_css: @@ -91,34 +117,16 @@ lang="${lang}"> % endif </%def> +### This function is deprecated; use feed_helper directly. <%def name="html_feedlinks()"> - %if rss_link: - ${rss_link} - %elif generate_rss: - %if len(translations) > 1: - %for language in sorted(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 sorted(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 + ${feeds_translations.head(classification=None, kind='index', other=False)} </%def> <%def name="html_translations()"> <ul class="translations"> %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> + <li><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li> %endif %endfor </ul> diff --git a/nikola/data/themes/base/templates/comments_helper.tmpl b/nikola/data/themes/base/templates/comments_helper.tmpl index 1459888..002499e 100644 --- a/nikola/data/themes/base/templates/comments_helper.tmpl +++ b/nikola/data/themes/base/templates/comments_helper.tmpl @@ -1,63 +1,63 @@ ## -*- coding: utf-8 -*- <%namespace name="disqus" file="comments_helper_disqus.tmpl"/> -<%namespace name="livefyre" file="comments_helper_livefyre.tmpl"/> <%namespace name="intensedebate" file="comments_helper_intensedebate.tmpl"/> <%namespace name="muut" file="comments_helper_muut.tmpl"/> -<%namespace name="googleplus" file="comments_helper_googleplus.tmpl"/> <%namespace name="facebook" file="comments_helper_facebook.tmpl"/> <%namespace name="isso" file="comments_helper_isso.tmpl"/> +<%namespace name="commento" file="comments_helper_commento.tmpl"/> +<%namespace name="utterances" file="comments_helper_utterances.tmpl"/> <%def name="comment_form(url, title, identifier)"> %if comment_system == 'disqus': ${disqus.comment_form(url, title, identifier)} - % elif comment_system == 'livefyre': - ${livefyre.comment_form(url, title, identifier)} % elif comment_system == 'intensedebate': ${intensedebate.comment_form(url, title, identifier)} % elif comment_system == 'muut': ${muut.comment_form(url, title, identifier)} - % elif comment_system == 'googleplus': - ${googleplus.comment_form(url, title, identifier)} % elif comment_system == 'facebook': ${facebook.comment_form(url, title, identifier)} % elif comment_system == 'isso': ${isso.comment_form(url, title, identifier)} + % elif comment_system == 'commento': + ${commento.comment_form(url, title, identifier)} + % elif comment_system == 'utterances': + ${utterances.comment_form(url, title, identifier)} %endif </%def> <%def name="comment_link(link, identifier)"> %if comment_system == 'disqus': ${disqus.comment_link(link, identifier)} - % elif comment_system == 'livefyre': - ${livefyre.comment_link(link, identifier)} % elif comment_system == 'intensedebate': ${intensedebate.comment_link(link, identifier)} % elif comment_system == 'muut': ${muut.comment_link(link, identifier)} - % elif comment_system == 'googleplus': - ${googleplus.comment_link(link, identifier)} % elif comment_system == 'facebook': ${facebook.comment_link(link, identifier)} % elif comment_system == 'isso': ${isso.comment_link(link, identifier)} + % elif comment_system == 'commento': + ${commento.comment_link(link, identifier)} + % elif comment_system == 'utterances': + ${utterances.comment_link(link, identifier)} %endif </%def> <%def name="comment_link_script()"> %if comment_system == 'disqus': ${disqus.comment_link_script()} - % elif comment_system == 'livefyre': - ${livefyre.comment_link_script()} % elif comment_system == 'intensedebate': ${intensedebate.comment_link_script()} % elif comment_system == 'muut': ${muut.comment_link_script()} - % elif comment_system == 'googleplus': - ${googleplus.comment_link_script()} % elif comment_system == 'facebook': ${facebook.comment_link_script()} % elif comment_system == 'isso': ${isso.comment_link_script()} + % elif comment_system == 'commento': + ${commento.comment_link_script()} + % elif comment_system == 'utterances': + ${utterances.comment_link_script()} %endif </%def> diff --git a/nikola/data/themes/base/templates/comments_helper_commento.tmpl b/nikola/data/themes/base/templates/comments_helper_commento.tmpl new file mode 100644 index 0000000..793a6d4 --- /dev/null +++ b/nikola/data/themes/base/templates/comments_helper_commento.tmpl @@ -0,0 +1,13 @@ +## -*- coding: utf-8 -*- +<%def name="comment_form(url, title, identifier)"> + <div id="commento"></div> + + <script defer src="${comment_system_id}/js/commento.js"></script> +</%def> + +<%def name="comment_link(link, identifier)"> + <a href="${link}#commento">${messages("Comments")}</a> +</%def> + +<%def name="comment_link_script()"> +</%def> diff --git a/nikola/data/themes/base/templates/comments_helper_disqus.tmpl b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl index b842871..f17777c 100644 --- a/nikola/data/themes/base/templates/comments_helper_disqus.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl @@ -32,7 +32,7 @@ <%def name="comment_link(link, identifier)"> %if comment_system_id: - <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">Comments</a> + <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">${messages("Comments")}</a> %endif </%def> diff --git a/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl b/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl deleted file mode 100644 index 5a5c4d7..0000000 --- a/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -## -*- coding: utf-8 -*- -<%def name="comment_form(url, title, identifier)"> -<script src="https://apis.google.com/js/plusone.js"></script> -<div class="g-comments" - data-href="${url}" - data-first_party_property="BLOGGER" - data-view_type="FILTERED_POSTMOD"> -</div> -</%def> - -<%def name="comment_link(link, identifier)"> -<div class="g-commentcount" data-href="${link}"></div> -<script src="https://apis.google.com/js/plusone.js"></script> -</%def> - -<%def name="comment_link_script()"> -</%def> diff --git a/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl index c47b6c7..0a51328 100644 --- a/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl @@ -6,7 +6,7 @@ var idcomments_post_id = "${identifier}"; var idcomments_post_url = "${url}"; </script> <span id="IDCommentsPostTitle" style="display:none"></span> -<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script> +<script src="https://www.intensedebate.com/js/genericCommentWrapperV2.js"></script> </script> </%def> @@ -17,7 +17,7 @@ var idcomments_acct = '${comment_system_id}'; var idcomments_post_id = "${identifier}"; var idcomments_post_url = "${link}"; </script> -<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> +<script src="https://www.intensedebate.com/js/genericLinkWrapperV2.js"></script> </a> </%def> diff --git a/nikola/data/themes/base/templates/comments_helper_isso.tmpl b/nikola/data/themes/base/templates/comments_helper_isso.tmpl index 95f93ae..c733bbe 100644 --- a/nikola/data/themes/base/templates/comments_helper_isso.tmpl +++ b/nikola/data/themes/base/templates/comments_helper_isso.tmpl @@ -1,20 +1,26 @@ ## -*- coding: utf-8 -*- <%def name="comment_form(url, title, identifier)"> %if comment_system_id: - <div data-title="${title|u}" id="isso-thread"></div> - <script src="${comment_system_id}js/embed.min.js" data-isso="${comment_system_id}"></script> + <div data-title="${title|h}" id="isso-thread"></div> + <script src="${comment_system_id}js/embed.min.js" data-isso="${comment_system_id}" data-isso-lang="${lang}" + % if isso_config: + % for k, v in isso_config.items(): + data-isso-${k}="${v}" + % endfor + % endif + ></script> %endif </%def> <%def name="comment_link(link, identifier)"> %if comment_system_id: - <a href="${link}#isso-thread">Comments</a> + <a href="${link}#isso-thread">${messages("Comments")}</a> %endif </%def> <%def name="comment_link_script()"> %if comment_system_id and 'index' in pagekind: - <script src="${comment_system_id}js/count.min.js" data-isso="${comment_system_id}"></script> + <script src="${comment_system_id}js/count.min.js" data-isso="${comment_system_id}" data-isso-lang="${lang}"></script> %endif </%def> diff --git a/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl b/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl deleted file mode 100644 index 68d99e5..0000000 --- a/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -## -*- coding: utf-8 -*- -<%def name="comment_form(url, title, identifier)"> -<div id="livefyre-comments"></div> -<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script> -<script> -(function () { - var articleId = "${identifier}"; - fyre.conv.load({}, [{ - el: 'livefyre-comments', - network: "livefyre.com", - siteId: "${comment_system_id}", - articleId: articleId, - signed: false, - collectionMeta: { - articleId: articleId, - url: fyre.conv.load.makeCollectionUrl(), - } - }], function() {}); -}()); -</script> -</%def> - -<%def name="comment_link(link, identifier)"> - <a href="${link}"> - <span class="livefyre-commentcount" data-lf-site-id="${comment_system_id}" data-lf-article-id="${identifier}"> - 0 Comments - </span> -</%def> - - -<%def name="comment_link_script()"> -<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script> -</%def> diff --git a/nikola/data/themes/base/templates/comments_helper_mustache.tmpl b/nikola/data/themes/base/templates/comments_helper_mustache.tmpl deleted file mode 100644 index 593d0aa..0000000 --- a/nikola/data/themes/base/templates/comments_helper_mustache.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace name="comments" file="comments_helper.tmpl"/> -% if not post.meta('nocomments'): - ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)} -% endif diff --git a/nikola/data/themes/base/templates/comments_helper_utterances.tmpl b/nikola/data/themes/base/templates/comments_helper_utterances.tmpl new file mode 100644 index 0000000..9b68917 --- /dev/null +++ b/nikola/data/themes/base/templates/comments_helper_utterances.tmpl @@ -0,0 +1,23 @@ +## -*- coding: utf-8 -*- +<%def name="comment_form(url, title, identifier)"> + %if comment_system_id: + <div data-title="${title|h}" id="utterances-thread"></div> + <script src="https://utteranc.es/client.js" repo="${comment_system_id}" + % if utterances_config: + % for k, v in utterances_config.items(): + ${k}="${v}" + % endfor + % endif + ></script> + %endif +</%def> + +<%def name="comment_link(link, identifier)"> + %if comment_system_id: + <a href="${link}#utterances-thread">${messages("Comments")}</a> + %endif +</%def> + + +<%def name="comment_link_script()"> +</%def> diff --git a/nikola/data/themes/base/templates/feeds_translations_helper.tmpl b/nikola/data/themes/base/templates/feeds_translations_helper.tmpl new file mode 100644 index 0000000..10e704d --- /dev/null +++ b/nikola/data/themes/base/templates/feeds_translations_helper.tmpl @@ -0,0 +1,124 @@ +## -*- coding: utf-8 -*- + +<%def name="_head_feed_link(link_type, link_name, link_postfix, classification, kind, language)"> + % if len(translations) > 1: + <link rel="alternate" type="${link_type}" title="${link_name|h} (${language})" hreflang="${language}" href="${_link(kind + '_' + link_postfix, classification, language)}"> + % else: + <link rel="alternate" type="${link_type}" title="${link_name|h}" hreflang="${language}" href="${_link(kind + '_' + link_postfix, classification, language)}"> + % endif +</%def> + +<%def name="_html_feed_link(link_type, link_name, link_postfix, classification, kind, language, name=None)"> + % if len(translations) > 1: + % if name and kind != "archive" and kind != "author": + <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${name|h}, ${language})</a> + % else: + <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${language})</a> + % endif + % else: + % if name and kind != "archive" and kind != "author": + <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${name|h})</a> + % else: + <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)}</a> + % endif + % endif +</%def> + +<%def name="_html_translation_link(classification, kind, language, name=None)"> + % if name and kind != "archive" and kind != "author": + <a href="${_link(kind, classification, language)}" hreflang="${language}" rel="alternate">${messages("LANGUAGE", language)} (${name|h})</a> + % else: + <a href="${_link(kind, classification, language)}" hreflang="${language}" rel="alternate">${messages("LANGUAGE", language)}</a> + % endif +</%def> + +<%def name="_head_rss(classification=None, kind='index', rss_override=True)"> + % if rss_link and rss_override: + ${rss_link} + % endif + % if generate_rss and not (rss_link and rss_override) and kind != 'archive': + % if len(translations) > 1 and has_other_languages and classification and kind != 'index': + % for language, classification, name in all_languages: + <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${name|h} (${language})" hreflang="${language}" href="${_link(kind + "_rss", classification, language)}"> + % endfor + % else: + % for language in translations_feedorder: + % if (classification or classification == '') and kind != 'index': + ${_head_feed_link('application/rss+xml', 'RSS for ' + kind + ' ' + classification, 'rss', classification, kind, language)} + % else: + ${_head_feed_link('application/rss+xml', 'RSS', 'rss', classification, 'index', language)} + % endif + % endfor + % endif + % endif +</%def> + +<%def name="_head_atom(classification=None, kind='index')"> + % if generate_atom: + % if len(translations) > 1 and has_other_languages and classification and kind != 'index': + % for language, classification, name in all_languages: + <link rel="alternate" type="application/atom+xml" title="Atom for ${kind} ${name|h} (${language})" hreflang="${language}" href="${_link(kind + "_atom", classification, language)}"> + % endfor + % else: + % for language in translations_feedorder: + % if (classification or classification == '') and kind != 'index': + ${_head_feed_link('application/atom+xml', 'Atom for ' + kind + ' ' + classification, 'atom', classification, kind, language)} + % else: + ${_head_feed_link('application/atom+xml', 'Atom', 'atom', classification, 'index', language)} + % endif + % endfor + % endif + % endif +</%def> + +## Handles both feeds and translations +<%def name="head(classification=None, kind='index', feeds=True, other=True, rss_override=True, has_no_feeds=False)"> + % if feeds and not has_no_feeds: + ${_head_rss(classification, 'index' if (kind == 'archive' and rss_override) else kind, rss_override)} + ${_head_atom(classification, kind)} + % endif + % if other and has_other_languages and other_languages: + % for language, classification, _ in other_languages: + <link rel="alternate" hreflang="${language}" href="${_link(kind, classification, language)}"> + % endfor + % endif +</%def> + +<%def name="feed_link(classification, kind)"> + % if generate_atom or generate_rss: + % if len(translations) > 1 and has_other_languages and kind != 'index': + % for language, classification, name in all_languages: + <p class="feedlink"> + % if generate_atom: + ${_html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language, name)} + % endif + % if generate_rss and kind != 'archive': + ${_html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language, name)} + % endif + </p> + % endfor + % else: + % for language in translations_feedorder: + <p class="feedlink"> + % if generate_atom: + ${_html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language)} + % endif + % if generate_rss and kind != 'archive': + ${_html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language)} + % endif + </p> + % endfor + % endif + % endif +</%def> + +<%def name="translation_link(kind)"> + % if has_other_languages and other_languages: + <div class="translationslist translations"> + <h3 class="translationslist-intro">${messages("Also available in:")}</h3> + % for language, classification, name in other_languages: + <p>${_html_translation_link(classification, kind, language, name)}</p> + % endfor + </div> + % endif +</%def> diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl index f9bbd1b..fef3a86 100644 --- a/nikola/data/themes/base/templates/gallery.tmpl +++ b/nikola/data/themes/base/templates/gallery.tmpl @@ -1,11 +1,12 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> -<%namespace name="ui" file="crumbs.tmpl" import="bar"/> +<%namespace name="ui" file="ui_helper.tmpl"/> +<%namespace name="post_helper" file="post_helper.tmpl"/> <%block name="sourcelink"></%block> <%block name="content"> - ${ui.bar(crumbs)} + ${ui.breadcrumbs(crumbs)} %if title: <h1>${title|h}</h1> %endif @@ -15,21 +16,39 @@ </p> %endif %if folders: - <ul> - % for folder, ftitle in folders: - <li><a href="${folder}"><i - class="icon-folder-open"></i> ${ftitle|h}</a></li> - % endfor - </ul> - %endif - %if photo_array: - <ul class="thumbnails"> - %for image in photo_array: - <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}"> - <img src="${image['url_thumb']}" alt="${image['title']|h}" /></a> - %endfor - </ul> - %endif + % if galleries_use_thumbnail: + % for (folder, ftitle, fpost) in folders: + <div class="thumnbnail-container"> + <a href="${folder}" class="thumbnail image-reference" title="${ftitle|h}"> + % if fpost and fpost.previewimage: + <img src="${fpost.previewimage}" alt="${ftitle|h}" loading="lazy" style="max-width:${thumbnail_size}px; max-height:${thumbnail_size}px;" /> + % else: + <div style="height: ${thumbnail_size}px; width: ${thumbnail_size}px; background-color: #eee;"></div> + % endif + <p class="thumbnail-caption">${ftitle|h}</p> + </a> + </div> + % endfor + % else: + <ul> + % for folder, ftitle in folders: + <li><a href="${folder}">📂 ${ftitle|h}</a></li> + % endfor + </ul> + % endif + % 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']|h}"> + <img src="${image['url_thumb']}" alt="${image['title']|h}" loading="lazy" /></a> + %endfor +</ul> +</noscript> +%endif %if site_has_comments and enable_comments: ${comments.comment_form(None, permalink, title)} %endif @@ -38,4 +57,35 @@ <%block name="extra_head"> ${parent.extra_head()} <link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> +<style type="text/css"> + #gallery_container { + position: relative; + } + .image-block { + position: absolute; + } +</style> +%if len(translations) > 1: + %for langname in translations.keys(): + %if langname != lang: + <link rel="alternate" hreflang="${langname}" href="${_link('gallery', gallery_path, langname)}"> + %endif + %endfor +%endif +<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml"> +%if post: + ${post_helper.open_graph_metadata(post)} + ${post_helper.twitter_card_information(post)} +%endif +</%block> + +<%block name="extra_js"> +<script src="/assets/js/justified-layout.min.js"></script> +<script src="/assets/js/gallery.min.js"></script> +<script> +var jsonContent = ${photo_array_json}; +var thumbnailSize = ${thumbnail_size}; +renderGallery(jsonContent, thumbnailSize); +window.addEventListener('resize', function(){renderGallery(jsonContent, thumbnailSize)}); +</script> </%block> diff --git a/nikola/data/themes/base/templates/index.tmpl b/nikola/data/themes/base/templates/index.tmpl index f74d2e4..b8e4f8c 100644 --- a/nikola/data/themes/base/templates/index.tmpl +++ b/nikola/data/themes/base/templates/index.tmpl @@ -1,6 +1,9 @@ ## -*- coding: utf-8 -*- <%namespace name="helper" file="index_helper.tmpl"/> +<%namespace name="math" file="math_helper.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> +<%namespace name="pagination" file="pagination_helper.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%inherit file="base.tmpl"/> <%block name="extra_head"> @@ -8,27 +11,45 @@ % if posts and (permalink == '/' or permalink == '/' + index_file): <link rel="prefetch" href="${posts[0].permalink()}" type="text/html"> % endif + ${math.math_styles_ifposts(posts)} </%block> <%block name="content"> -<%block name="content_header"></%block> +<%block name="content_header"> + ${feeds_translations.translation_link(kind)} +</%block> % if 'main_index' in pagekind: ${front_index_header} % endif +% if page_links: + ${pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed)} +% endif <div class="postindex"> % for post in posts: - <article class="h-entry post-${post.meta('type')}"> + <article class="h-entry post-${post.meta('type')}" itemscope="itemscope" itemtype="http://schema.org/Article"> <header> <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"> - % if author_pages_generated: + <p class="byline author vcard"><span class="byline-name fn" itemprop="author"> + % if author_pages_generated and multiple_authors_per_post: + % for author in post.authors(): + <a href="${_link('author', author)}">${author|h}</a> + % endfor + % elif author_pages_generated: <a href="${_link('author', post.author())}">${post.author()|h}</a> % else: ${post.author()|h} % endif </span></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)|h}">${post.formatted_date(date_format)|h}</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)|h}">${post.formatted_date(date_format)|h}</time> + % if post.updated and post.updated != post.date: + <span class="updated"> (${messages("updated")} + <time class="dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span> + % endif + </a> + </p> % if not post.meta('nocomments') and site_has_comments: <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)} % endif @@ -47,5 +68,5 @@ </div> ${helper.html_pager()} ${comments.comment_link_script()} -${helper.mathjax_script(posts)} +${math.math_scripts_ifposts(posts)} </%block> diff --git a/nikola/data/themes/base/templates/index_helper.tmpl b/nikola/data/themes/base/templates/index_helper.tmpl index 0e98016..e400e3b 100644 --- a/nikola/data/themes/base/templates/index_helper.tmpl +++ b/nikola/data/themes/base/templates/index_helper.tmpl @@ -1,4 +1,5 @@ ## -*- coding: utf-8 -*- +<%namespace name="math" file="math_helper.tmpl"/> <%def name="html_pager()"> %if prevlink or nextlink: <nav class="postindexpager"> @@ -18,33 +19,7 @@ %endif </%def> +### This function is deprecated; use math_helper directly. <%def name="mathjax_script(posts)"> - %if any(post.is_mathjax for post in posts): - %if use_katex: - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script> - % if katex_auto_render: - <script> - renderMathInElement(document.body, - { - ${katex_auto_render} - } - ); - </script> - % else: - <script> - renderMathInElement(document.body); - </script> - % endif - %else: - <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script> - % if mathjax_config: - ${mathjax_config} - % else: - <script type="text/x-mathjax-config"> - MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); - </script> - % endif - %endif - %endif + ${math.math_scripts_ifposts(posts)} </%def> diff --git a/nikola/data/themes/base/templates/list.tmpl b/nikola/data/themes/base/templates/list.tmpl index 5a8843d..ca6c421 100644 --- a/nikola/data/themes/base/templates/list.tmpl +++ b/nikola/data/themes/base/templates/list.tmpl @@ -1,11 +1,19 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> +<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="extra_head"> + ${feeds_translations.head(kind=kind, rss_override=False, has_no_feeds=has_no_feeds)} +</%block> <%block name="content"> <article class="listpage"> <header> <h1>${title|h}</h1> </header> + ${archive_nav.archive_navigation()} + ${feeds_translations.translation_link(kind)} %if items: <ul class="postlist"> % for text, link, count in items: diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl index bc52385..8cd9336 100644 --- a/nikola/data/themes/base/templates/list_post.tmpl +++ b/nikola/data/themes/base/templates/list_post.tmpl @@ -1,11 +1,19 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> +<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="extra_head"> + ${feeds_translations.head(kind=kind, rss_override=False)} +</%block> <%block name="content"> <article class="listpage"> <header> <h1>${title|h}</h1> </header> + ${archive_nav.archive_navigation()} + ${feeds_translations.translation_link(kind)} %if posts: <ul class="postlist"> % for post in posts: diff --git a/nikola/data/themes/base/templates/listing.tmpl b/nikola/data/themes/base/templates/listing.tmpl index fae7607..ef2dfd6 100644 --- a/nikola/data/themes/base/templates/listing.tmpl +++ b/nikola/data/themes/base/templates/listing.tmpl @@ -1,15 +1,15 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> -<%namespace name="ui" file="crumbs.tmpl" import="bar"/> +<%namespace name="ui" file="ui_helper.tmpl"/> <%block name="content"> -${ui.bar(crumbs)} +${ui.breadcrumbs(crumbs)} %if folders or files: <ul> % for name in folders: - <li><a href="${name|u}"><i class="icon-folder-open"></i> ${name|h}</a> + <li><a href="${name|h}" class="listing-folder">${name|h}</a> % endfor % for name in files: - <li><a href="${name|u}.html"><i class="icon-file"></i> ${name|h}</a> + <li><a href="${name|h}.html" class="listing-file">${name|h}</a> % endfor </ul> %endif @@ -22,5 +22,3 @@ ${ui.bar(crumbs)} ${code} % endif </%block> - - diff --git a/nikola/data/themes/base/templates/math_helper.tmpl b/nikola/data/themes/base/templates/math_helper.tmpl new file mode 100644 index 0000000..961b7ce --- /dev/null +++ b/nikola/data/themes/base/templates/math_helper.tmpl @@ -0,0 +1,69 @@ +### Note: at present, MathJax and KaTeX do not respect the USE_CDN configuration option +<%def name="math_scripts()"> + %if use_katex: + <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js" integrity="sha384-9Nhn55MVVN0/4OFx7EE5kpFBPsEMZxKTCnA+4fqDmg12eCTqGi6+BB2LjY8brQxJ" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"></script> + % if katex_auto_render: + <script> + renderMathInElement(document.body, + { + ${katex_auto_render} + } + ); + </script> + % else: + <script> + renderMathInElement(document.body, + { + delimiters: [ + {left: "$$", right: "$$", display: true}, + {left: "\\[", right: "\\]", display: true}, + {left: "\\begin{equation*}", right: "\\end{equation*}", display: true}, + {left: "\\(", right: "\\)", display: false} + ] + } + ); + </script> + % endif + %else: +### Note: given the size of MathJax; nikola will retrieve MathJax from a CDN regardless of use_cdn configuration + <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML" integrity="sha384-3lJUsx1TJHt7BA4udB5KPnDrlkO8T6J6v/op7ui0BbCjvZ9WqV4Xm6DTP6kQ/iBH" crossorigin="anonymous"></script> + % if mathjax_config: + ${mathjax_config} + % else: + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); + </script> + % endif + %endif +</%def> + +<%def name="math_styles()"> + % if use_katex: + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css" integrity="sha384-yFRtMMDnQtDRO8rLpMIKrtPCD5jdktao2TV19YiZYWMDkUR5GQZR/NOVTdquEx1j" crossorigin="anonymous"> + % endif +</%def> + +<%def name="math_scripts_ifpost(post)"> + %if post.has_math: + ${math_scripts()} + %endif +</%def> + +<%def name="math_scripts_ifposts(posts)"> + %if any(post.has_math for post in posts): + ${math_scripts()} + %endif +</%def> + +<%def name="math_styles_ifpost(post)"> + %if post.has_math: + ${math_styles()} + %endif +</%def> + +<%def name="math_styles_ifposts(posts)"> + %if any(post.has_math for post in posts): + ${math_styles()} + %endif +</%def> diff --git a/nikola/data/themes/base/templates/page.tmpl b/nikola/data/themes/base/templates/page.tmpl new file mode 100644 index 0000000..b2cd756 --- /dev/null +++ b/nikola/data/themes/base/templates/page.tmpl @@ -0,0 +1 @@ +<%inherit file="story.tmpl"/> diff --git a/nikola/data/themes/base/templates/pagination_helper.tmpl b/nikola/data/themes/base/templates/pagination_helper.tmpl new file mode 100644 index 0000000..91c1115 --- /dev/null +++ b/nikola/data/themes/base/templates/pagination_helper.tmpl @@ -0,0 +1,16 @@ +## -*- coding: utf-8 -*- +<%def name="page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5)"> +<div class="page-navigation"> + % for i, link in enumerate(page_links): + % if abs(i - current_page) <= surrounding or i == 0 or i == len(page_links) - 1: + % if i == current_page: + <span class="current-page">${i+1}</span> + % else: + <a href="${page_links[i]}">${i+1}</a> + % endif + % elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1: + <span class="ellipsis">…</span> + % endif + % endfor +</div> +</%def> diff --git a/nikola/data/themes/base/templates/post.tmpl b/nikola/data/themes/base/templates/post.tmpl index da616bf..1f2f0a4 100644 --- a/nikola/data/themes/base/templates/post.tmpl +++ b/nikola/data/themes/base/templates/post.tmpl @@ -2,16 +2,14 @@ <%namespace name="helper" file="post_helper.tmpl"/> <%namespace name="pheader" file="post_header.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> +<%namespace name="math" file="math_helper.tmpl"/> <%inherit file="base.tmpl"/> <%block name="extra_head"> ${parent.extra_head()} % if post.meta('keywords'): - <meta name="keywords" content="${post.meta('keywords')|h}"> + <meta name="keywords" content="${smartjoin(', ', post.meta('keywords'))|h}"> % endif - %if post.description(): - <meta name="description" content="${post.description()|h}"> - %endif <meta name="author" content="${post.author()|h}"> %if post.prev_post: <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html"> @@ -25,6 +23,7 @@ ${helper.open_graph_metadata(post)} ${helper.twitter_card_information(post)} ${helper.meta_translations(post)} + ${math.math_styles_ifpost(post)} </%block> <%block name="content"> @@ -45,7 +44,7 @@ ${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)} </section> % endif - ${helper.mathjax_script(post)} + ${math.math_scripts_ifpost(post)} </article> ${comments.comment_link_script()} </%block> diff --git a/nikola/data/themes/base/templates/post_header.tmpl b/nikola/data/themes/base/templates/post_header.tmpl index 480c36a..617a156 100644 --- a/nikola/data/themes/base/templates/post_header.tmpl +++ b/nikola/data/themes/base/templates/post_header.tmpl @@ -23,7 +23,7 @@ <%def name="html_sourcelink()"> % if show_sourcelink: - <p class="sourceline"><a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a></p> + <p class="sourceline"><a href="${post.source_link()}" class="sourcelink">${messages("Source")}</a></p> % endif </%def> @@ -31,14 +31,26 @@ <header> ${html_title()} <div class="metadata"> - <p class="byline author vcard"><span class="byline-name fn"> - % if author_pages_generated: - <a href="${_link('author', post.author())}">${post.author()|h}</a> + <p class="byline author vcard p-author h-card"><span class="byline-name fn p-name" itemprop="author"> + % if author_pages_generated and multiple_authors_per_post: + % for author in post.authors(): + <a class="u-url" href="${_link('author', author)}">${author|h}</a> + % endfor + % elif author_pages_generated: + <a class="u-url" href="${_link('author', post.author())}">${post.author()|h}</a> % else: ${post.author()|h} % endif </span></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)|h}">${post.formatted_date(date_format)|h}</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)|h}">${post.formatted_date(date_format)|h}</time> + % if post.updated and post.updated != post.date: + <span class="updated"> (${messages("updated")} + <time class="updated dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span> + % endif + </a> + </p> % if not post.meta('nocomments') and site_has_comments: <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)} % endif @@ -46,9 +58,6 @@ % if post.meta('link'): <p class="linkline"><a href="${post.meta('link')}">${messages("Original site")}</a></p> % endif - %if post.description(): - <meta name="description" itemprop="description" content="${post.description()|h}"> - %endif </div> ${html_translations(post)} </header> diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl index 47bf9b3..9ae4489 100644 --- a/nikola/data/themes/base/templates/post_helper.tmpl +++ b/nikola/data/themes/base/templates/post_helper.tmpl @@ -1,4 +1,5 @@ ## -*- coding: utf-8 -*- +<%namespace name="math" file="math_helper.tmpl"/> <%def name="meta_translations(post)"> %if len(translations) > 1: @@ -40,31 +41,29 @@ </%def> <%def name="open_graph_metadata(post)"> -%if use_open_graph: - <meta property="og:site_name" content="${blog_title|h}"> - <meta property="og:title" content="${post.title()[:70]|h}"> - <meta property="og:url" content="${abs_link(permalink)}"> - %if post.description(): +<meta property="og:site_name" content="${blog_title|h}"> +<meta property="og:title" content="${post.title()[:70]|h}"> +<meta property="og:url" content="${abs_link(permalink)}"> +%if post.description(): <meta property="og:description" content="${post.description()[:200]|h}"> - %else: +%else: <meta property="og:description" content="${post.text(strip_html=True)[:200]|h}"> - %endif - %if post.previewimage: +%endif +%if post.previewimage: <meta property="og:image" content="${url_replacer(permalink, post.previewimage, lang, 'absolute')}"> - %endif - <meta property="og:type" content="article"> +%endif +<meta property="og:type" content="article"> ### Will only work with Pintrest and breaks everywhere else who expect a [Facebook] URI. ### %if post.author(): ### <meta property="article:author" content="${post.author()|h}"> ### %endif - %if post.date.isoformat(): +%if post.date.isoformat(): <meta property="article:published_time" content="${post.formatted_date('webiso')}"> - %endif - %if post.tags: - %for tag in post.tags: - <meta property="article:tag" content="${tag|h}"> - %endfor - %endif +%endif +%if post.tags: + %for tag in post.tags: + <meta property="article:tag" content="${tag|h}"> + %endfor %endif </%def> @@ -84,33 +83,7 @@ %endif </%def> +### This function is deprecated; use math_helper directly. <%def name="mathjax_script(post)"> - %if post.is_mathjax: - %if use_katex: - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script> - % if katex_auto_render: - <script> - renderMathInElement(document.body, - { - ${katex_auto_render} - } - ); - </script> - % else: - <script> - renderMathInElement(document.body); - </script> - % endif - %else: - <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script> - % if mathjax_config: - ${mathjax_config} - % else: - <script type="text/x-mathjax-config"> - MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}}); - </script> - % endif - %endif - %endif + ${math.math_scripts_ifpost(post)} </%def> diff --git a/nikola/data/themes/base/templates/sectionindex.tmpl b/nikola/data/themes/base/templates/sectionindex.tmpl deleted file mode 100644 index 7fb4f1e..0000000 --- a/nikola/data/themes/base/templates/sectionindex.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="index.tmpl"/> - -<%block name="extra_head"> - ${parent.extra_head()} - % if generate_atom: - <link rel="alternate" type="application/atom+xml" title="Atom for the ${posts[0].section_name()|h} section" href="${_link('section_index_atom', posts[0].section_slug())}"> - % endif -</%block> - -<%block name="content"> -<div class="sectionindex"> - <header> - <h2><a href="${_link('section_index', posts[0].section_slug())}">${title|h}</a></h2> - % if generate_atom: - <p class="feedlink"><a href="${_link('section_index_atom', posts[0].section_slug())}" type="application/atom+xml">${messages('Updates')}</a></p> - % endif - </header> - ${parent.content()} -</div> -</%block> diff --git a/nikola/data/themes/base/templates/slides.tmpl b/nikola/data/themes/base/templates/slides.tmpl deleted file mode 100644 index 048fb7e..0000000 --- a/nikola/data/themes/base/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/base/templates/story.tmpl b/nikola/data/themes/base/templates/story.tmpl index b8fb7ed..aeac04f 100644 --- a/nikola/data/themes/base/templates/story.tmpl +++ b/nikola/data/themes/base/templates/story.tmpl @@ -2,6 +2,7 @@ <%namespace name="helper" file="post_helper.tmpl"/> <%namespace name="pheader" file="post_header.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> +<%namespace name="math" file="math_helper.tmpl"/> <%inherit file="post.tmpl"/> <%block name="content"> @@ -19,6 +20,6 @@ ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)} </section> %endif - ${helper.mathjax_script(post)} + ${math.math_scripts_ifpost(post)} </article> </%block> diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl index 50c5bf2..ac40876 100644 --- a/nikola/data/themes/base/templates/tag.tmpl +++ b/nikola/data/themes/base/templates/tag.tmpl @@ -1,24 +1,17 @@ ## -*- coding: utf-8 -*- <%inherit file="list_post.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%block name="extra_head"> - ${parent.extra_head()} - %if len(translations) > 1 and generate_rss: - %for language in sorted(translations): - <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${tag|h} (${language})" href="${_link(kind + "_rss", tag, language)}"> - %endfor - %elif generate_rss: - <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${tag|h}" href="${_link(kind + "_rss", tag)}"> - %endif + ${feeds_translations.head(tag, kind, rss_override=False)} </%block> - <%block name="content"> <article class="tagpage"> <header> <h1>${title|h}</h1> %if description: - <p>${description}</p> + <p>${description}</p> %endif %if subcategories: ${messages('Subcategories:')} @@ -29,23 +22,16 @@ </ul> %endif <div class="metadata"> - %if len(translations) > 1 and generate_rss: - %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> - %endfor - %elif generate_rss: - <p class="feedlink"><a href="${_link(kind + "_rss", tag)}" type="application/rss+xml">${messages('RSS feed')}</a></p> - %endif + ${feeds_translations.feed_link(tag, kind=kind)} </div> + ${feeds_translations.translation_link(kind)} </header> %if posts: - <ul class="postlist"> - % for post in posts: - <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}<a></li> - % endfor - </ul> + <ul class="postlist"> + % for post in posts: + <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}<a></li> + % endfor + </ul> %endif </article> </%block> diff --git a/nikola/data/themes/base/templates/tagindex.tmpl b/nikola/data/themes/base/templates/tagindex.tmpl index c3c51b0..232d093 100644 --- a/nikola/data/themes/base/templates/tagindex.tmpl +++ b/nikola/data/themes/base/templates/tagindex.tmpl @@ -1,5 +1,6 @@ ## -*- coding: utf-8 -*- <%inherit file="index.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> <%block name="content_header"> <header> @@ -15,16 +16,14 @@ %endfor </ul> %endif + <div class="metadata"> + ${feeds_translations.feed_link(tag, kind)} + ${feeds_translations.translation_link(kind)} + </div> </header> </%block> <%block name="extra_head"> ${parent.extra_head()} - %if len(translations) > 1 and generate_atom: - %for language in sorted(translations): - <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag|h} section (${language})" href="${_link(kind + "_atom", tag, language)}"> - %endfor - %elif generate_atom: - <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag|h} section" href="${_link("tag" + "_atom", tag)}"> - %endif + ${feeds_translations.head(tag, kind, rss_override=False)} </%block> diff --git a/nikola/data/themes/base/templates/tags.tmpl b/nikola/data/themes/base/templates/tags.tmpl index 6c329d9..c54559a 100644 --- a/nikola/data/themes/base/templates/tags.tmpl +++ b/nikola/data/themes/base/templates/tags.tmpl @@ -1,10 +1,18 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="extra_head"> + ${feeds_translations.head(kind=kind, feeds=False)} +</%block> <%block name="content"> <article class="tagindex"> <header> <h1>${title|h}</h1> + <div class="metadata"> + ${feeds_translations.translation_link(kind)} + </div> </header> % if cat_items: % if items: diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/ui_helper.tmpl index 49c5e1e..173027c 100644 --- a/nikola/data/themes/base/templates/crumbs.tmpl +++ b/nikola/data/themes/base/templates/ui_helper.tmpl @@ -1,6 +1,5 @@ ## -*- coding: utf-8 -*- - -<%def name="bar(crumbs)"> +<%def name="breadcrumbs(crumbs)"> %if crumbs: <nav class="breadcrumbs"> <ul class="breadcrumb"> diff --git a/nikola/data/themes/bootblog4-jinja/README.md b/nikola/data/themes/bootblog4-jinja/README.md new file mode 100644 index 0000000..6a9226e --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/README.md @@ -0,0 +1,6 @@ +This is a theme based on Bootstrap 4 and the [blog example](https://getbootstrap.com/docs/4.0/examples/blog/) by @mdo. + +Note that unlike previous versions of Bootstrap, icon fonts are not built-in. +You can use Font Awesome for this. + +This theme **does not** support Bootswatch font/color schemes. diff --git a/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css b/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css new file mode 120000 index 0000000..c8bd66a --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css @@ -0,0 +1 @@ +../../../bootblog4/assets/css/bootblog.css
\ No newline at end of file diff --git a/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme b/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme new file mode 100644 index 0000000..8a5e55f --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme @@ -0,0 +1,12 @@ +[Theme] +engine = jinja +parent = bootstrap4-jinja +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT +based_on = Bootstrap 4 <http://getbootstrap.com/>, Bootstrap 4 blog example <http://getbootstrap.com/docs/4.0/examples/blog/> +tags = bootstrap + +[Family] +family = bootblog4 +mako-version = bootstrap4 diff --git a/nikola/data/themes/bootblog4-jinja/bundles b/nikola/data/themes/bootblog4-jinja/bundles new file mode 120000 index 0000000..94a0160 --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/bundles @@ -0,0 +1 @@ +../bootblog4/bundles
\ No newline at end of file diff --git a/nikola/data/themes/bootblog4-jinja/templates/base.tmpl b/nikola/data/themes/bootblog4-jinja/templates/base.tmpl new file mode 100644 index 0000000..0adf447 --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/templates/base.tmpl @@ -0,0 +1,104 @@ +{# -*- 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> + +<!-- Header and menu bar --> +<div class="container"> + <header class="blog-header py-3"> + <div class="row nbb-header align-items-center"> + <div class="col-md-3 col-xs-2 col-sm-2" style="width: auto;"> + <button class="navbar-toggler navbar-light bg-light nbb-navbar-toggler" type="button" data-toggle="collapse" data-target=".bs-nav-collapsible" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse bs-nav-collapsible bootblog4-search-form-holder"> + {{ search_form }} + </div> + </div> + <div class="col-md-6 col-xs-10 col-sm-10 bootblog4-brand" style="width: auto;"> + <a class="navbar-brand blog-header-logo text-dark" href="{{ _link("root", None, lang) }}"> + {% if logo_url %} + <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo" class="d-inline-block align-top"> + {% endif %} + + {% if show_blog_title %} + <span id="blog-title">{{ blog_title|e }}</span> + {% endif %} + </a> + </div> + <div class="col-md-3 justify-content-end align-items-center bs-nav-collapsible collapse flex-collapse bootblog4-right-nav"> + <nav class="navbar navbar-light bg-white"> + <ul class="navbar-nav bootblog4-right-nav"> + {{ base.html_navigation_links_entries(navigation_alt_links) }} + {% block belowtitle %} + {% if translations|length > 1 %} + {{ base.html_translations() }} + {% endif %} + {% endblock %} + {% block sourcelink %}{% endblock %} + {{ template_hooks['menu_alt']() }} + </ul></nav> + </div> + </div> +</header> + +<nav class="navbar navbar-expand-md navbar-light bg-white static-top"> + <div class="collapse navbar-collapse bs-nav-collapsible" id="bs-navbar"> + <ul class="navbar-nav nav-fill d-flex w-100"> + {{ base.html_navigation_links_entries(navigation_links) }} + {{ template_hooks['menu']() }} + </ul> + </div><!-- /.navbar-collapse --> +</nav> +{% block before_content %}{% endblock %} +</div> + +<div class="container" id="content" role="main"> + <div class="body-content"> + {% if theme_config.get('sidebar') %} + <div class="row"><div class="col-md-8 blog-main"> + {% endif %} + <!--Body content--> + {{ template_hooks['page_header']() }} + {% block extra_header %}{% endblock %} + {% block content %}{% endblock %} + <!--End of body content--> + {% if theme_config.get('sidebar') %} + </div><aside class="col-md-4 blog-sidebar">{{ theme_config.get('sidebar') }}</aside></div> + {% endif %} + + <footer id="footer"> + {{ content_footer }} + {{ template_hooks['page_footer']() }} + {% block extra_footer %}{% endblock %} + </footer> + </div> +</div> + +{{ base.late_load_js() }} + {% if date_fanciness != 0 %} + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}"; + fancydates({{ date_fanciness }}, {{ luxon_date_format }}); + </script> + <!-- end fancy dates --> + {% endif %} + {% block extra_js %}{% endblock %} + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> +{{ body_end }} +{{ template_hooks['body_end']() }} +</body> +</html> diff --git a/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl new file mode 100644 index 0000000..0b74696 --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl @@ -0,0 +1,169 @@ +{# -*- coding: utf-8 -*- #} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% macro html_headstart() %} +<!DOCTYPE html> +<html + +prefix=' +og: http://ogp.me/ns# article: http://ogp.me/ns/article# +{% if comment_system == 'facebook' %} +fb: http://ogp.me/ns/fb# +{% endif %} +' +{% if is_rtl %} +dir="rtl" +{% endif %} + +lang="{{ lang }}"> + <head> + <meta charset="utf-8"> + {% if description %} + <meta name="description" content="{{ description|e }}"> + {% endif %} + <meta name="viewport" content="width=device-width, initial-scale=1"> + {% if title == blog_title %} + <title>{{ blog_title|e }}</title> + {% else %} + <title>{{ title|e }} | {{ blog_title|e }}</title> + {% endif %} + + {{ html_stylesheets() }} + <meta name="theme-color" content="{{ theme_color }}"> + {% if meta_generator_tag %} + <meta name="generator" content="Nikola (getnikola.com)"> + {% endif %} + {{ html_feedlinks() }} + <link rel="canonical" href="{{ abs_link(permalink) }}"> + + {% 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 %} + + {% if use_cdn %} + <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + {% else %} + <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang, url_type) }}"></script><![endif]--> + {% endif %} + + {{ extra_head_data }} +{% endmacro %} + +{% macro late_load_js() %} + {% if use_cdn %} + <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + {% endif %} + {% if use_bundles and use_cdn %} + <script src="/assets/js/all.js"></script> + {% elif use_bundles %} + <script src="/assets/js/all-nocdn.js"></script> + {% else %} + {% if not use_cdn %} + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/popper.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + <script src="/assets/js/baguetteBox.min.js"></script> + {% endif %} + {% endif %} + {% if date_fanciness != 0 %} + {% if date_fanciness == 2 %} + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script> + {% endif %} + {% if use_cdn %} + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + {% else %} + <script src="/assets/js/luxon.min.js"></script> + {% endif %} + {% if not use_bundles %} + <script src="/assets/js/fancydates.min.js"></script> + {% endif %} + {% endif %} + {{ social_buttons_code }} +{% endmacro %} + + +{% macro html_stylesheets() %} + {% if use_cdn %} + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> + {% endif %} + {% if use_bundles and use_cdn %} + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + {% elif use_bundles %} + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + {% else %} + {% if not use_cdn %} + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.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/theme.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootblog.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 %} + <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"> +{% endmacro %} + +{% macro html_navigation_links() %} + {{ html_navigation_links_entries(navigation_links) }} +{% endmacro %} + +{% macro html_navigation_links_entries(navigation_links_source) %} + {% for url, text in navigation_links_source[lang] %} + {% if isinstance(url, tuple) %} + <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }}</a> + <div class="dropdown-menu"> + {% for suburl, text in url %} + {% if rel_link(permalink, suburl) == "#" %} + <a href="{{ permalink }}" class="dropdown-item active">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a> + {% else %} + <a href="{{ suburl }}" class="dropdown-item">{{ text }}</a> + {% endif %} + {% endfor %} + </div> + {% else %} + {% if rel_link(permalink, url) == "#" %} + <li class="nav-item active"><a href="{{ permalink }}" class="nav-link">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a> + {% else %} + <li class="nav-item"><a href="{{ url }}" class="nav-link">{{ text }}</a> + {% endif %} + {% endif %} + {% endfor %} +{% endmacro %} + + + +{% macro html_feedlinks() %} + {{ feeds_translations.head(classification=None, kind='index', other=False) }} +{% endmacro %} + +{% macro html_translations() %} + {% for langname in translations|sort %} + {% if langname != lang %} + <li class="nav-item"><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}" class="nav-link">{{ messages("LANGUAGE", langname) }}</a></li> + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/nikola/data/themes/bootblog4-jinja/templates/index.tmpl b/nikola/data/themes/bootblog4-jinja/templates/index.tmpl new file mode 100644 index 0000000..efc4e58 --- /dev/null +++ b/nikola/data/themes/bootblog4-jinja/templates/index.tmpl @@ -0,0 +1,150 @@ +{# -*- coding: utf-8 -*- #} +{% import 'index_helper.tmpl' as helper with context %} +{% import 'math_helper.tmpl' as math with context %} +{% import 'comments_helper.tmpl' as comments with context %} +{% import 'pagination_helper.tmpl' as pagination with context %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} +{% extends 'base.tmpl' %} + +{% block extra_head %} + {{ super() }} + {% if posts and (permalink == '/' or permalink == '/' + index_file) %} + <link rel="prefetch" href="{{ posts[0].permalink() }}" type="text/html"> + {% endif %} + {{ math.math_styles_ifposts(posts) }} +{% endblock %} + +{% block content %} + {% block content_header %} + {{ feeds_translations.translation_link(kind) }} + {% endblock %} + {% if 'main_index' in pagekind %} + {{ front_index_header }} + {% endif %} + {% if page_links %} + {{ pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed) }} + {% endif %} + <div class="postindex"> + {% for post in posts %} + <article class="h-entry post-{{ post.meta('type') }}" itemscope="itemscope" itemtype="http://schema.org/Article"> + <header> + <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" itemprop="author"> + {% if author_pages_generated and multiple_authors_per_post %} + {% for author in post.authors() %} + <a href="{{ _link('author', author) }}">{{ author|e }}</a> + {% endfor %} + {% elif author_pages_generated %} + <a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a> + {% else %} + {{ post.author()|e }} + {% endif %} + </span></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)|e }}">{{ post.formatted_date(date_format)|e }}</time> + {% if post.updated and post.updated != post.date %} + <span class="updated"> ({{ messages("updated") }} + <time class="dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span> + {% endif %} + </a> + </p> + {% if not post.meta('nocomments') and site_has_comments %} + <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }} + {% endif %} + </div> + </header> + {% if index_teasers %} + <div class="p-summary entry-summary"> + {{ post.text(teaser_only=True) }} + </div> + {% else %} + <div class="e-content entry-content"> + {{ post.text(teaser_only=False) }} + </div> + {% endif %} + </article> + {% endfor %} + </div> + {{ helper.html_pager() }} + {{ comments.comment_link_script() }} + {{ math.math_scripts_ifposts(posts) }} +{% endblock %} + +{% block before_content %} + {% if 'main_index' in pagekind and is_frontmost_index and featured and (theme_config.get('featured_large') or theme_config.get('featured_small')) %} + {% if theme_config.get('featured_on_mobile') %} + <div class="d-block"> + {% else %} + <div class="d-none d-md-block"> + {% endif %} + {% if featured and theme_config.get('featured_large') %} + <div class="jumbotron p-0 text-white rounded bg-dark"> + <div class="row bootblog4-featured-jumbotron-row"> + <div class="col-md-6 p-3 p-md-4 pr-0 h-md-250 bootblog4-featured-text"> + <h1 class="display-4 font-italic"><a class="text-white" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a></h1> + {% if featured[0].previewimage %} + <div class="lead my-3 mb-0">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + {% if theme_config.get('featured_large_image_on_mobile') %} + <div class="col-md-6 p-0 h-md-250 text-right"> + {% else %} + <div class="col-md-6 p-0 h-md-250 text-right d-none d-md-block"> + {% endif %} + <img class="bootblog4-featured-large-image" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}"> + </div> + {% else %} + <div class="lead my-3 mb-0">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + {% endif %} + </div> + </div> + {% endif %} + + {% if featured and theme_config.get('featured_small') %} + <div class="row mb-2"> + {% if featured|length == 1 %} + <div class="col-md-12"> + {% else %} + <div class="col-md-6"> + {% endif %} + <div class="card flex-md-row mb-4 box-shadow h-md-250"> + <div class="card-body d-flex flex-column align-items-start"> + <h3 class="mb-0"> + <a class="text-dark" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a> + </h3> + {% if featured[0].previewimage %} + <div class="card-text mb-auto bootblog4-featured-text">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + <img class="card-img-right flex-auto d-none d-lg-block" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}"> + {% else %} + <div class="card-text mb-auto bootblog4-featured-text">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + {% endif %} + </div> + </div> + + {% if featured %} + <div class="col-md-6"> + <div class="card flex-md-row mb-4 box-shadow h-md-250"> + <div class="card-body d-flex flex-column align-items-start"> + <h3 class="mb-0"> + <a class="text-dark" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a> + </h3> + {% if featured[0].previewimage %} + <div class="card-text mb-auto bootblog4-featured-text">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + <img class="card-img-right flex-auto d-none d-lg-block" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}"> + {% else %} + <div class="card-text mb-auto bootblog4-featured-text">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div> + </div> + {% endif %} + </div> + </div> + {% endif %} + </div> + {% endif %} + </div> +{% endif %} +{% endblock %} diff --git a/nikola/data/themes/bootblog4/README.md b/nikola/data/themes/bootblog4/README.md new file mode 100644 index 0000000..6a9226e --- /dev/null +++ b/nikola/data/themes/bootblog4/README.md @@ -0,0 +1,6 @@ +This is a theme based on Bootstrap 4 and the [blog example](https://getbootstrap.com/docs/4.0/examples/blog/) by @mdo. + +Note that unlike previous versions of Bootstrap, icon fonts are not built-in. +You can use Font Awesome for this. + +This theme **does not** support Bootswatch font/color schemes. diff --git a/nikola/data/themes/bootblog4/assets/css/bootblog.css b/nikola/data/themes/bootblog4/assets/css/bootblog.css new file mode 100644 index 0000000..96d4b92 --- /dev/null +++ b/nikola/data/themes/bootblog4/assets/css/bootblog.css @@ -0,0 +1,225 @@ +/* stylelint-disable selector-list-comma-newline-after */ + +.blog-header { + line-height: 1; + border-bottom: 1px solid #e5e5e5; +} + +.blog-header-logo { + font-family: "Playfair Display", Georgia, "Times New Roman", serif; + font-size: 2.25rem; +} + +.blog-header-logo:hover { + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "Playfair Display", Georgia, "Times New Roman", serif; +} + +.display-4 { + font-size: 2.5rem; +} +@media (min-width: 768px) { + .display-4 { + font-size: 3rem; + } +} + +.nav-scroller { + position: relative; + z-index: 2; + height: 2.75rem; + overflow-y: hidden; +} + +.nav-scroller .nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + padding-bottom: 1rem; + margin-top: -1px; + overflow-x: auto; + text-align: center; + white-space: nowrap; + -webkit-overflow-scrolling: touch; +} + +.nav-scroller .nav-link { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: .875rem; +} + +.card-img-right { + height: 100%; + border-radius: 0 3px 3px 0; +} + +.flex-auto { + -ms-flex: 0 0 auto; + -webkit-box-flex: 0; + flex: 0 0 auto; +} + +.h-150 { height: 150px; } +@media (min-width: 768px) { + .h-md-150 { height: 150px; } +} + +.h-250 { height: 250px; } +@media (min-width: 768px) { + .h-md-250 { height: 250px; } +} + +.border-top { border-top: 1px solid #e5e5e5; } +.border-bottom { border-bottom: 1px solid #e5e5e5; } + +.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); } + +/* + * Blog name and description + */ +.blog-title { + margin-bottom: 0; + font-size: 2rem; + font-weight: 400; +} +.blog-description { + font-size: 1.1rem; + color: #999; +} + +@media (min-width: 40em) { + .blog-title { + font-size: 3.5rem; + } +} + +/* Pagination */ +.blog-pagination { + margin-bottom: 4rem; +} +.blog-pagination > .btn { + border-radius: 2rem; +} + +/* + * Blog posts + */ +article { + margin-bottom: 4rem; +} +article:last-child { + margin-bottom: 0; +} +.entry-title { + margin-bottom: .25rem; + font-size: 2.5rem; +} +article .metadata { + margin-bottom: 1.25rem; + color: #999; +} + +/* + * Footer + */ +.blog-footer { + padding: 2.5rem 0; + color: #999; + text-align: center; + background-color: #f9f9f9; + border-top: .05rem solid #e5e5e5; +} +.blog-footer p:last-child { + margin-bottom: 0; +} + +@media (min-width: 576px) { + .nbb-navbar-toggler { + display: none; + } + + .nbb-header { + -webkit-box-pack: justify!important; + -ms-flex-pack: justify!important; + justify-content: space-between!important; + } +} + +/* Various fixes that make this theme look better for Nikola's needs */ +.navbar-brand { + padding: 0; + white-space: initial; +} + +.bootblog4-featured-large-image { + height: 100%; + border-top-right-radius: .25rem!important; + border-bottom-right-radius: .25rem!important; +} + +.bootblog4-featured-jumbotron-row { + margin-left: 0; + margin-right: 0; +} + +.bootblog4-right-nav { + flex-direction: row; +} + +.bootblog4-right-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; +} + +.bootblog4-featured-text { + overflow: auto; +} +/* extend the mobile appearance to `sm`, because otherwise weird things happen */ +@media (min-width: 576px) { + .nbb-navbar-toggler { + display: block; + } +} + +@media (max-width: 767px) { + .bootblog4-right-nav { + margin-top: 1rem; + } + + .bootblog4-search-form-holder { + position: absolute; + top: 2.75rem; + } + + .bootblog4-search-form-holder input.form-control { + width: 6rem; + } + + .bootblog4-brand { + text-align: left; + } +} + +@media (min-width: 768px) { + .nbb-navbar-toggler { + display: none; + } + + .flex-collapse { + display: flex !important; + } + + .bootblog4-search-form-holder { + display: block !important; + } + + .bootblog4-brand { + text-align: center; + } +} diff --git a/nikola/data/themes/bootblog4/bootblog4.theme b/nikola/data/themes/bootblog4/bootblog4.theme new file mode 100644 index 0000000..46db4ea --- /dev/null +++ b/nikola/data/themes/bootblog4/bootblog4.theme @@ -0,0 +1,12 @@ +[Theme] +engine = mako +parent = bootstrap4 +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT +based_on = Bootstrap 4 <http://getbootstrap.com/>, Bootstrap 4 blog example <http://getbootstrap.com/docs/4.0/examples/blog/> +tags = bootstrap + +[Family] +family = bootblog4 +jinja_version = bootblog4-jinja diff --git a/nikola/data/themes/bootblog4/bundles b/nikola/data/themes/bootblog4/bundles new file mode 100644 index 0000000..76ffd4b --- /dev/null +++ b/nikola/data/themes/bootblog4/bundles @@ -0,0 +1,28 @@ +; css bundles +assets/css/all-nocdn.css= + bootstrap.min.css, + rst_base.css, + nikola_rst.css, + code.css, + baguetteBox.min.css, + theme.css, + bootblog.css, + custom.css, +assets/css/all.css= + rst_base.css, + nikola_rst.css, + code.css, + baguetteBox.min.css, + theme.css, + bootblog.css, + custom.css, + +; javascript bundles +assets/js/all-nocdn.js= + jquery.min.js, + popper.min.js, + bootstrap.min.js, + baguetteBox.min.js, + fancydates.min.js, +assets/js/all.js= + fancydates.min.js, diff --git a/nikola/data/themes/bootblog4/templates/base.tmpl b/nikola/data/themes/bootblog4/templates/base.tmpl new file mode 100644 index 0000000..69b9d30 --- /dev/null +++ b/nikola/data/themes/bootblog4/templates/base.tmpl @@ -0,0 +1,104 @@ +## -*- 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> + +<!-- Header and menu bar --> +<div class="container"> + <header class="blog-header py-3"> + <div class="row nbb-header align-items-center"> + <div class="col-md-3 col-xs-2 col-sm-2" style="width: auto;"> + <button class="navbar-toggler navbar-light bg-light nbb-navbar-toggler" type="button" data-toggle="collapse" data-target=".bs-nav-collapsible" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse bs-nav-collapsible bootblog4-search-form-holder"> + ${search_form} + </div> + </div> + <div class="col-md-6 col-xs-10 col-sm-10 bootblog4-brand" style="width: auto;"> + <a class="navbar-brand blog-header-logo text-dark" href="${_link("root", None, lang)}"> + %if logo_url: + <img src="${logo_url}" alt="${blog_title|h}" id="logo" class="d-inline-block align-top"> + %endif + + % if show_blog_title: + <span id="blog-title">${blog_title|h}</span> + % endif + </a> + </div> + <div class="col-md-3 justify-content-end align-items-center bs-nav-collapsible collapse flex-collapse bootblog4-right-nav"> + <nav class="navbar navbar-light bg-white"> + <ul class="navbar-nav bootblog4-right-nav"> + ${base.html_navigation_links_entries(navigation_alt_links)} + <%block name="belowtitle"> + %if len(translations) > 1: + ${base.html_translations()} + %endif + </%block> + <%block name="sourcelink"></%block> + ${template_hooks['menu_alt']()} + </ul></nav> + </div> + </div> +</header> + +<nav class="navbar navbar-expand-md navbar-light bg-white static-top"> + <div class="collapse navbar-collapse bs-nav-collapsible" id="bs-navbar"> + <ul class="navbar-nav nav-fill d-flex w-100"> + ${base.html_navigation_links_entries(navigation_links)} + ${template_hooks['menu']()} + </ul> + </div><!-- /.navbar-collapse --> +</nav> +<%block name="before_content"></%block> +</div> + +<div class="container" id="content" role="main"> + <div class="body-content"> + % if theme_config.get('sidebar'): + <div class="row"><div class="col-md-8 blog-main"> + % endif + <!--Body content--> + ${template_hooks['page_header']()} + <%block name="extra_header"></%block> + <%block name="content"></%block> + <!--End of body content--> + % if theme_config.get('sidebar'): + </div><aside class="col-md-4 blog-sidebar">${theme_config.get('sidebar')}</aside></div> + % endif + + <footer id="footer"> + ${content_footer} + ${template_hooks['page_footer']()} + <%block name="extra_footer"></%block> + </footer> + </div> +</div> + +${base.late_load_js()} + %if date_fanciness != 0: + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "${luxon_locales[lang]}"; + fancydates(${date_fanciness}, ${luxon_date_format}); + </script> + <!-- end fancy dates --> + %endif + <%block name="extra_js"></%block> + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> +${body_end} +${template_hooks['body_end']()} +</body> +</html> diff --git a/nikola/data/themes/bootblog4/templates/base_helper.tmpl b/nikola/data/themes/bootblog4/templates/base_helper.tmpl new file mode 100644 index 0000000..3c919b4 --- /dev/null +++ b/nikola/data/themes/bootblog4/templates/base_helper.tmpl @@ -0,0 +1,169 @@ +## -*- coding: utf-8 -*- +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%def name="html_headstart()"> +<!DOCTYPE html> +<html +\ +prefix='\ +og: http://ogp.me/ns# article: http://ogp.me/ns/article# \ +%if comment_system == 'facebook': +fb: http://ogp.me/ns/fb# \ +%endif +'\ +% if is_rtl: +dir="rtl" \ +% endif +\ +lang="${lang}"> + <head> + <meta charset="utf-8"> + %if description: + <meta name="description" content="${description|h}"> + %endif + <meta name="viewport" content="width=device-width, initial-scale=1"> + %if title == blog_title: + <title>${blog_title|h}</title> + %else: + <title>${title|h} | ${blog_title|h}</title> + %endif + + ${html_stylesheets()} + <meta name="theme-color" content="${theme_color}"> + % if meta_generator_tag: + <meta name="generator" content="Nikola (getnikola.com)"> + % endif + ${html_feedlinks()} + <link rel="canonical" href="${abs_link(permalink)}"> + + %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 + + %if use_cdn: + <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + %else: + <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang, url_type)}"></script><![endif]--> + %endif + + ${extra_head_data} +</%def> + +<%def name="late_load_js()"> + %if use_cdn: + <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + % endif + %if use_bundles and use_cdn: + <script src="/assets/js/all.js"></script> + %elif use_bundles: + <script src="/assets/js/all-nocdn.js"></script> + %else: + %if not use_cdn: + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/popper.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + <script src="/assets/js/baguetteBox.min.js"></script> + %endif + %endif + %if date_fanciness != 0: + %if date_fanciness == 2: + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script> + %endif + %if use_cdn: + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + %else: + <script src="/assets/js/luxon.min.js"></script> + %endif + %if not use_bundles: + <script src="/assets/js/fancydates.min.js"></script> + %endif + %endif + ${social_buttons_code} +</%def> + + +<%def name="html_stylesheets()"> + % if use_cdn: + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> + % endif + %if use_bundles and use_cdn: + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + %elif use_bundles: + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + %else: + %if not use_cdn: + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.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/theme.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootblog.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 + <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"> +</%def> + +<%def name="html_navigation_links()"> + ${html_navigation_links_entries(navigation_links)} +</%def> + +<%def name="html_navigation_links_entries(navigation_links_source)"> + %for url, text in navigation_links_source[lang]: + % if isinstance(url, tuple): + <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text}</a> + <div class="dropdown-menu"> + %for suburl, text in url: + % if rel_link(permalink, suburl) == "#": + <a href="${permalink}" class="dropdown-item active">${text} <span class="sr-only">${messages("(active)", lang)}</span></a> + %else: + <a href="${suburl}" class="dropdown-item">${text}</a> + %endif + %endfor + </div> + % else: + % if rel_link(permalink, url) == "#": + <li class="nav-item active"><a href="${permalink}" class="nav-link">${text} <span class="sr-only">${messages("(active)", lang)}</span></a> + %else: + <li class="nav-item"><a href="${url}" class="nav-link">${text}</a> + %endif + % endif + %endfor +</%def> + + + +<%def name="html_feedlinks()"> + ${feeds_translations.head(classification=None, kind='index', other=False)} +</%def> + +<%def name="html_translations()"> + %for langname in sorted(translations): + %if langname != lang: + <li class="nav-item"><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}" class="nav-link">${messages("LANGUAGE", langname)}</a></li> + %endif + %endfor +</%def> diff --git a/nikola/data/themes/bootblog4/templates/index.tmpl b/nikola/data/themes/bootblog4/templates/index.tmpl new file mode 100644 index 0000000..449c5ec --- /dev/null +++ b/nikola/data/themes/bootblog4/templates/index.tmpl @@ -0,0 +1,150 @@ +## -*- coding: utf-8 -*- +<%namespace name="helper" file="index_helper.tmpl"/> +<%namespace name="math" file="math_helper.tmpl"/> +<%namespace name="comments" file="comments_helper.tmpl"/> +<%namespace name="pagination" file="pagination_helper.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> +<%inherit file="base.tmpl"/> + +<%block name="extra_head"> + ${parent.extra_head()} + % if posts and (permalink == '/' or permalink == '/' + index_file): + <link rel="prefetch" href="${posts[0].permalink()}" type="text/html"> + % endif + ${math.math_styles_ifposts(posts)} +</%block> + +<%block name="content"> + <%block name="content_header"> + ${feeds_translations.translation_link(kind)} + </%block> + % if 'main_index' in pagekind: + ${front_index_header} + % endif + % if page_links: + ${pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed)} + % endif + <div class="postindex"> + % for post in posts: + <article class="h-entry post-${post.meta('type')}" itemscope="itemscope" itemtype="http://schema.org/Article"> + <header> + <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" itemprop="author"> + % if author_pages_generated and multiple_authors_per_post: + % for author in post.authors(): + <a href="${_link('author', author)}">${author|h}</a> + % endfor + % elif author_pages_generated: + <a href="${_link('author', post.author())}">${post.author()|h}</a> + % else: + ${post.author()|h} + % endif + </span></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)|h}">${post.formatted_date(date_format)|h}</time> + % if post.updated and post.updated != post.date: + <span class="updated"> (${messages("updated")} + <time class="dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span> + % endif + </a> + </p> + % if not post.meta('nocomments') and site_has_comments: + <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)} + % endif + </div> + </header> + %if index_teasers: + <div class="p-summary entry-summary"> + ${post.text(teaser_only=True)} + </div> + %else: + <div class="e-content entry-content"> + ${post.text(teaser_only=False)} + </div> + %endif + </article> + % endfor + </div> + ${helper.html_pager()} + ${comments.comment_link_script()} + ${math.math_scripts_ifposts(posts)} +</%block> + +<%block name="before_content"> + % if 'main_index' in pagekind and is_frontmost_index and featured and (theme_config.get('featured_large') or theme_config.get('featured_small')): + % if theme_config.get('featured_on_mobile'): + <div class="d-block"> + % else: + <div class="d-none d-md-block"> + % endif + % if featured and theme_config.get('featured_large'): + <div class="jumbotron p-0 text-white rounded bg-dark"> + <div class="row bootblog4-featured-jumbotron-row"> + <div class="col-md-6 p-3 p-md-4 pr-0 h-md-250 bootblog4-featured-text"> + <h1 class="display-4 font-italic"><a class="text-white" href="${featured[0].permalink()}">${featured[0].title()}</a></h1> + % if featured[0].previewimage: + <div class="lead my-3 mb-0">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + % if theme_config.get('featured_large_image_on_mobile'): + <div class="col-md-6 p-0 h-md-250 text-right"> + % else: + <div class="col-md-6 p-0 h-md-250 text-right d-none d-md-block"> + % endif + <img class="bootblog4-featured-large-image" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}"> + </div> + % else: + <div class="lead my-3 mb-0">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + % endif + </div> + </div> + % endif + + % if featured and theme_config.get('featured_small'): + <div class="row mb-2"> + % if len(featured) == 1: + <div class="col-md-12"> + % else: + <div class="col-md-6"> + % endif + <div class="card flex-md-row mb-4 box-shadow h-md-250"> + <div class="card-body d-flex flex-column align-items-start"> + <h3 class="mb-0"> + <a class="text-dark" href="${featured[0].permalink()}">${featured[0].title()}</a> + </h3> + % if featured[0].previewimage: + <div class="card-text mb-auto bootblog4-featured-text">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + <img class="card-img-right flex-auto d-none d-lg-block" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}"> + % else: + <div class="card-text mb-auto bootblog4-featured-text">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + % endif + </div> + </div> + + % if featured: + <div class="col-md-6"> + <div class="card flex-md-row mb-4 box-shadow h-md-250"> + <div class="card-body d-flex flex-column align-items-start"> + <h3 class="mb-0"> + <a class="text-dark" href="${featured[0].permalink()}">${featured[0].title()}</a> + </h3> + % if featured[0].previewimage: + <div class="card-text mb-auto bootblog4-featured-text">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + <img class="card-img-right flex-auto d-none d-lg-block" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}"> + % else: + <div class="card-text mb-auto bootblog4-featured-text">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div> + </div> + % endif + </div> + </div> + % endif + </div> + %endif + </div> +% endif +</%block> diff --git a/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt b/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt deleted file mode 100644 index 043d497..0000000 --- a/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt +++ /dev/null @@ -1 +0,0 @@ -Roberto Alsina <https://github.com/ralsina> diff --git a/nikola/data/themes/bootstrap3-jinja/README.md b/nikola/data/themes/bootstrap3-jinja/README.md deleted file mode 100644 index 10e673a..0000000 --- a/nikola/data/themes/bootstrap3-jinja/README.md +++ /dev/null @@ -1,8 +0,0 @@ -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 -developers at least). This one uses the default bootstrap3 flat look. - -This theme supports Bootswtach font/color schemes (unlike -bootstrap3-gradients) through the `nikola bootswatch_theme` command. diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css deleted file mode 120000 index 78d39af..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map deleted file mode 120000 index 639bdc1..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css deleted file mode 120000 index 200c765..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map deleted file mode 120000 index fcd3722..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css deleted file mode 120000 index 013623e..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map deleted file mode 120000 index 8448a3d..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css deleted file mode 120000 index 5bc6076..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map deleted file mode 120000 index 5914aca..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css b/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css deleted file mode 120000 index 5f8b3b0..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/example3/colorbox.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css b/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css deleted file mode 100644 index 189ea89..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css +++ /dev/null @@ -1,160 +0,0 @@ -body { - font-weight: 300; -} - -a:hover, -a:focus { - text-decoration: none; -} - -.container { - max-width: 700px; -} - -h2 { - text-align: center; - font-weight: 300; -} - - -/* Header --------------------------------------------------- */ - -.jumbotron { - position: relative; - font-size: 16px; - color: #fff; - color: rgba(255,255,255,.75); - text-align: center; - background-color: #b94a48; - border-radius: 0; -} -.jumbotron h1, -.jumbotron .glyphicon-ok { - margin-bottom: 15px; - font-weight: 300; - letter-spacing: -1px; - color: #fff; -} -.jumbotron .glyphicon-ok { - font-size: 40px; - line-height: 1; -} -.btn-outline { - margin-top: 15px; - margin-bottom: 15px; - padding: 18px 24px; - font-size: inherit; - font-weight: 500; - color: #fff; /* redeclare to override the `.jumbotron a` */ - background-color: transparent; - border-color: #fff; - border-color: rgba(255,255,255,.5); - transition: all .1s ease-in-out; -} -.btn-outline:hover, -.btn-outline:active { - color: #b94a48; - background-color: #fff; - border-color: #fff; -} - -.jumbotron:after { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 10; - display: block; - content: ""; - height: 30px; - background-image: -moz-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1)); - background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1)); -} - -.jumbotron p a, -.jumbotron-links a { - font-weight: 500; - color: #fff; - transition: all .1s ease-in-out; -} -.jumbotron p a:hover, -.jumbotron-links a:hover { - text-shadow: 0 0 10px rgba(255,255,255,.55); -} - -/* Textual links */ -.jumbotron-links { - margin-top: 15px; - margin-bottom: 0; - padding-left: 0; - list-style: none; - font-size: 14px; -} -.jumbotron-links li { - display: inline; -} -.jumbotron-links li + li { - margin-left: 20px; -} - -@media (min-width: 768px) { - .jumbotron { - padding-top: 100px; - padding-bottom: 100px; - font-size: 21px; - } - .jumbotron h1, - .jumbotron .glyphicon-ok { - font-size: 50px; - } -} - -/* Steps for setup --------------------------------------------------- */ - -.how-to { - padding: 50px 20px; - border-top: 1px solid #eee; -} -.how-to li { - font-size: 21px; - line-height: 1.5; - margin-top: 20px; -} -.how-to li p { - font-size: 16px; - color: #555; -} -.how-to code { - font-size: 85%; - color: #b94a48; - background-color: #fcf3f2; - word-wrap: break-word; - white-space: normal; -} - -/* Icons --------------------------------------------------- */ - -.the-icons { - padding: 40px 10px; - font-size: 20px; - line-height: 2; - color: #333; - text-align: center; -} -.the-icons .glyphicon { - padding-left: 15px; - padding-right: 15px; -} - -/* Footer --------------------------------------------------- */ - -.footer { - padding: 50px 30px; - color: #777; - text-align: center; - border-top: 1px solid #eee; -} diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png deleted file mode 120000 index 841a726..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/example3/images/controls.png
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png Binary files differdeleted file mode 100644 index 0d4475e..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png Binary files differdeleted file mode 100644 index 2775eba..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png Binary files differdeleted file mode 100644 index f7f5137..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png Binary files differdeleted file mode 100644 index a2d63d1..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png Binary files differdeleted file mode 100644 index fd7c3e8..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png Binary files differdeleted file mode 100644 index 2937a9c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png Binary files differdeleted file mode 100644 index f9d458b..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png Binary files differdeleted file mode 100644 index 74b8583..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif deleted file mode 120000 index b192a75..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/example3/images/loading.gif
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot deleted file mode 120000 index c2dfd17..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg deleted file mode 120000 index 30abe9d..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf deleted file mode 120000 index 93e3bf3..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff deleted file mode 120000 index f7595ae..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 deleted file mode 120000 index 8c1e4d3..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js b/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js deleted file mode 120000 index 26aa1fd..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/js/bootstrap.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js deleted file mode 120000 index c4cdf6c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/js/bootstrap.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js deleted file mode 120000 index f83073f..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js deleted file mode 120000 index bafc4e0..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js deleted file mode 120000 index 9b995d8..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js deleted file mode 120000 index a749232..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js deleted file mode 120000 index e4a595c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js deleted file mode 120000 index 1e9a1d6..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js deleted file mode 120000 index 748f53b..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js deleted file mode 120000 index 1154fb5..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js deleted file mode 120000 index 483e192..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js deleted file mode 120000 index a30b13c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js deleted file mode 120000 index 2a7e8ad..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js deleted file mode 120000 index e359290..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js deleted file mode 120000 index 04fa276..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js deleted file mode 120000 index d8105ab..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js deleted file mode 120000 index 72dddf5..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js deleted file mode 120000 index 34aa3c0..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js deleted file mode 120000 index a87f03c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js deleted file mode 120000 index 31053b8..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js deleted file mode 120000 index aad9d22..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js deleted file mode 120000 index 3ea27c2..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js deleted file mode 120000 index 3e23b4a..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js deleted file mode 120000 index 374b9bb..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js deleted file mode 120000 index 101b476..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js deleted file mode 120000 index 8e14f15..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js deleted file mode 120000 index 2d03d48..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js deleted file mode 120000 index 9af0ba7..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js deleted file mode 120000 index 34f8ab1..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-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 deleted file mode 120000 index e20bd38..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js deleted file mode 120000 index 555f2e6..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js deleted file mode 120000 index bac4855..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js deleted file mode 120000 index 65b0492..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js deleted file mode 120000 index 99859fd..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js deleted file mode 120000 index c4fd9d5..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js deleted file mode 120000 index d7f26e0..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js deleted file mode 120000 index 86fd98f..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js deleted file mode 120000 index 7cd1336..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-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 deleted file mode 120000 index e6c5965..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-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 deleted file mode 120000 index bd2254c..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js deleted file mode 100644 index 732fa3d..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Flowr.js - Simple jQuery plugin to emulate Flickr's justified view - * For usage information refer to http://github.com/kalyan02/flowr-js - * - * - * @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net) - * @version: v0.1 - */ -(function($) { - //$("#container2").css( 'border', '1px solid #ccc'); - $.fn.flowr = function(options) { - - $this = this; - var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy - var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed - var NO_COPY_FIELDS = ['complete', 'data', 'responsive']; // these attributes will not be carried forward for append related calls - var DEFAULTS = { - 'data': [], - 'padding': 5, // whats the padding between flowy items - 'height': 240, // Minimum height an image row should take - 'render': null, // callback function to get the tag - 'append': false, // TODO - 'widthAttr': 'width', // a custom data structure can specify which attribute refers to height/width - 'heightAttr': 'height', - 'maxScale': 1.5, // In case there is only 1 elment in last row - 'maxWidth': this.width() - 1, // 1px is just for offset - 'itemWidth': null, // callback function for width - 'itemHeight': null, // callback function for height - 'complete': null, // complete callback - 'rowClassName': ROW_CLASS_NAME, - 'rows': -1, // Maximum number of rows to render. -1 for no limit. - 'responsive': true // make content responsive - }; - var settings = $.extend(DEFAULTS, options); - - // If data is being appended, we already have settings - // If we already have settings, retrieve them - if (settings.append && $this.data('lastSettings')) { - lastSettings = $this.data('lastSettings'); - - // Copy over the settings from previous init - for (attr in DEFAULTS) { - if (NO_COPY_FIELDS.indexOf(attr) < 0 && settings[attr] == DEFAULTS[attr]) { - settings[attr] = lastSettings[attr]; - } - } - - // Check if we have an incomplete last row - lastRow = $this.data('lastRow'); - if (lastRow.data.length > 0 && settings.maxWidth - lastRow.width > MAX_LAST_ROW_GAP) { - // Prepend the incomplete row to newly loaded data and redraw - lastRowData = lastSettings.data.slice(lastSettings.data.length - lastRow.data.length - 1); - settings.data = lastRowData.concat(settings.data); - - // Remove the incomplete row - // TODO: Don't reload this stuff later. Reattach to new row. - $('.' + settings.rowClassName + ':last', $this).detach(); - } else { - // console.log( lastRow.data.length ); - // console.log( lastRow.width ); - } - } - - // only on the first initial call - if (!settings.responsive && !settings.append) - $this.width($this.width()); - - // Basic sanity checks - if (!(settings.data instanceof Array)) - return; - - if (typeof(settings.padding) != 'number') - settings.padding = parseInt(settings.padding); - - if (typeof(settings.itemWidth) != 'function') { - settings.itemWidth = function(data) { - return data[settings.widthAttr]; - } - } - - if (typeof(settings.itemHeight) != 'function') { - settings.itemHeight = function(data) { - return data[settings.heightAttr]; - } - } - - // A standalone utility to calculate the item widths for a particular row - // Returns rowWidth: width occupied & data : the items in the new row - var utils = { - getNextRow: function(data, settings) { - var itemIndex = 0; - var itemsLength = data.length; - var lineItems = []; - var lineWidth = 0; - var maxWidth = settings.maxWidth; - var paddingSize = settings.padding; - - // console.log( 'maxItems=' + data.length ); - - requiredPadding = function() { - var extraPads = arguments.length == 1 ? arguments[0] : 0; - return (lineItems.length - 1 + extraPads) * settings.padding; - } - - while (lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength)) { - var itemData = data[itemIndex]; - var itemWidth = settings.itemWidth.call($this, itemData); - var itemHeight = settings.itemHeight.call($this, itemData); - - var minHeight = settings.height; - var minWidth = Math.floor(itemWidth * settings.height / itemHeight); - - - if (minWidth > settings.maxWidth) { - // very short+wide images like panoramas - // show them even if ugly, as wide as possible - minWidth = settings.maxWidth - 1 - requiredPadding(1); - minHeight = settings.height * minHeight / minWidth; - } - var newLineWidth = lineWidth + minWidth; - - // console.log( 'lineWidth = ' + lineWidth ); - // console.log( 'newLineWidth = ' + newLineWidth ); - if (newLineWidth < settings.maxWidth) { - lineItems.push({ - 'height': minHeight, - 'width': minWidth, - 'itemData': itemData - }); - - lineWidth += minWidth; - itemIndex++; - } else { - // We'd have exceeded width. So break off to scale. - // console.log( 'breaking off = ' + itemIndex ); - // console.log( 'leave off size = ' + lineItems.length ); - break; - } - } //while - - // Scale the size to max width - testWidth = 0; - if (lineWidth < settings.maxWidth) { - var fullScaleWidth = settings.maxWidth - requiredPadding() - 10; - var currScaleWidth = lineWidth; - var scaleFactor = fullScaleWidth / currScaleWidth; - if (scaleFactor > settings.maxScale) - scaleFactor = 1; - - var newHeight = Math.round(settings.height * scaleFactor); - for (i = 0; i < lineItems.length; i++) { - var lineItem = lineItems[i]; - lineItem.width = Math.floor(lineItem.width * scaleFactor); - lineItem.height = newHeight; - - testWidth += lineItem.width; - } - } - - return { - data: lineItems, - width: testWidth + requiredPadding() - }; - }, //getNextRow - reorderContent: function() { - /* - TODO: optimize for faster resizing by reusing dom objects instead of killing the dom - */ - var _initialWidth = $this.data('width'); - var _newWidth = $this.width(); - var _change = _initialWidth - _newWidth; - - if (_initialWidth != _newWidth) { - $this.html(''); - var _settings = $this.data('lastSettings'); - _settings.data = $this.data('data'); - _settings.maxWidth = $this.width() - 1; - $this.flowr(_settings); - } - } - } //utils - - // If the responsive var is set to true then listen for resize method - // and prevent resizing from happening twice if responsive is set again during append phase! - if (settings.responsive && !$this.data('__responsive')) { - $(window).resize(function() { - initialWidth = $this.data('width'); - newWidth = $this.width(); - - //initiate resize - if (initialWidth != newWidth) { - var task_id = $this.data('task_id'); - if (task_id) { - task_id = clearTimeout(task_id); - task_id = null; - } - task_id = setTimeout(utils.reorderContent, 80); - $this.data('task_id', task_id); - } - }); - $this.data('__responsive', true); - } - - - return this.each(function() { - - // Get a copy of original data. 1 level deep copy is sufficient. - var data = settings.data.slice(0); - var rowData = null; - var currentRow = 0; - var currentItem = 0; - - // Store all the data - var allData = []; - for (i = 0; i < data.length; i++) { - allData.push(data[i]); - } - $this.data('data', allData); - - // While we have a new row - while ((rowData = utils.getNextRow(data, settings)) != null && rowData.data.length > 0) { - if (settings.rows > 0 && currentRow >= settings.rows) - break; - // remove the number of elements in the new row from the top of data stack - data.splice(0, rowData.data.length); - - // Create a new row div, add class, append the htmls and insert the flowy items - var $row = $('<div>').addClass(settings.rowClassName); - var slack = $this[0].clientWidth - rowData.width - 2 * settings.padding - for (i = 0; i < rowData.data.length; i++) { - var displayData = rowData.data[i]; - // Get the HTML object from custom render function passed as argument - var displayObject = settings.render.call($this, displayData); - displayObject = $(displayObject); - extraw = Math.floor(slack/rowData.data.length) - if (i == 0) { - extraw += slack % rowData.data.length - } - // Set some basic stuff - displayObject - .css('width', displayData.width + extraw) - .css('height', displayData.height) - .css('margin-bottom', settings.padding + "px") - .css('margin-left', i == 0 ? '0' : settings.padding + "px"); //TODO:Refactor - $row.append(displayObject); - - currentItem++; - } - $this.append($row); - // console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width ); - - currentRow++; - $this.data('lastRow', rowData); - } - // store the current state of settings and the items in last row - // we'll need this info when we append more items - $this.data('lastSettings', settings); - - // onComplete callback - // pass back info about list of rows and items rendered - if (typeof(settings.complete) == 'function') { - var completeData = { - renderedRows: currentRow, - renderedItems: currentItem - } - settings.complete.call($this, completeData); - } - }); - }; - -})(jQuery); diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js deleted file mode 120000 index 9e40fd4..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/jquery.colorbox-min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js deleted file mode 120000 index 5ee7a90..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js deleted file mode 120000 index 966173b..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js deleted file mode 120000 index 5c080da..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map deleted file mode 120000 index 7e2c217..0000000 --- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.min.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/bundles b/nikola/data/themes/bootstrap3-jinja/bundles deleted file mode 120000 index 8cb3e06..0000000 --- a/nikola/data/themes/bootstrap3-jinja/bundles +++ /dev/null @@ -1 +0,0 @@ -../bootstrap3/bundles
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/engine b/nikola/data/themes/bootstrap3-jinja/engine deleted file mode 100644 index 6f04b30..0000000 --- a/nikola/data/themes/bootstrap3-jinja/engine +++ /dev/null @@ -1 +0,0 @@ -jinja diff --git a/nikola/data/themes/bootstrap3-jinja/parent b/nikola/data/themes/bootstrap3-jinja/parent deleted file mode 100644 index e9ed660..0000000 --- a/nikola/data/themes/bootstrap3-jinja/parent +++ /dev/null @@ -1 +0,0 @@ -base-jinja diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl deleted file mode 100644 index 4ce46d0..0000000 --- a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl +++ /dev/null @@ -1,94 +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 --> - -<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 collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false"> - <span class="sr-only">{{ messages("Toggle navigation") }}</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a class="navbar-brand" href="{{ abs_link(_link("root", None, lang)) }}"> - {% if logo_url %} - <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo"> - {% endif %} - - {% if show_blog_title %} - <span id="blog-title">{{ blog_title|e }}</span> - {% endif %} - </a> - </div><!-- /.navbar-header --> - <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false"> - <ul class="nav navbar-nav"> - {{ base.html_navigation_links() }} - {{ template_hooks['menu']() }} - </ul> - {% if search_form %} - {{ search_form }} - {% endif %} - - <ul class="nav navbar-nav navbar-right"> - {% block belowtitle %} - {% if translations|length > 1 %} - <li>{{ base.html_translations() }}</li> - {% endif %} - {% endblock %} - {% if show_sourcelink %} - {% block sourcelink %}{% endblock %} - {% endif %} - {{ template_hooks['menu_alt']() }} - </ul> - </div><!-- /.navbar-collapse --> - </div><!-- /.container --> -</nav> - -<!-- End of Menubar --> - -<div class="container" id="content" role="main"> - <div class="body-content"> - <!--Body content--> - <div class="row"> - {{ template_hooks['page_header']() }} - {% block content %}{% endblock %} - </div> - <!--End of body content--> - - <footer id="footer"> - {{ content_footer }} - {{ template_hooks['page_footer']() }} - </footer> - </div> -</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/bootstrap3-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl deleted file mode 100644 index 1d1802f..0000000 --- a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl +++ /dev/null @@ -1,188 +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 use_base_tag %} - <base href="{{ abs_link(permalink) }}"> - {% endif %} - {% if description %} - <meta name="description" content="{{ description|e }}"> - {% endif %} - <meta name="viewport" content="width=device-width, initial-scale=1"> - {% if title == blog_title %} - <title>{{ blog_title|e }}</title> - {% else %} - <title>{{ title|e }} | {{ blog_title|e }}</title> - {% endif %} - - {{ html_stylesheets() }} - <meta content="{{ theme_color }}" name="theme-color"> - {{ html_feedlinks() }} - <link rel="canonical" href="{{ abs_link(permalink) }}"> - - {% 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="https://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="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> - - <script src="/assets/js/all.js"></script> - {% else %} - <script src="/assets/js/all-nocdn.js"></script> - {% endif %} - {% else %} - {% if use_cdn %} - <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - <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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - {% else %} - <link href="/assets/css/bootstrap.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" 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) == "#" %} - <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|sort %} - <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|sort %} - <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|sort %} - {% 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/bootstrap3-jinja/templates/gallery.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl deleted file mode 100644 index cd9a5ed..0000000 --- a/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl +++ /dev/null @@ -1,95 +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="glyphicon glyphicon-folder-open"></i> {{ ftitle|e }}</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']|e }}"> - <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" /></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}); -$('a.image-reference[href="'+window.location.hash.substring(1,1000)+'"]').click(); -</script> -{% endblock %} diff --git a/nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl deleted file mode 100644 index 342ed27..0000000 --- a/nikola/data/themes/bootstrap3-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"><span class="icon-prev"></span></a> - <a class="right carousel-control" href="#{{ carousel_id }}" data-slide="next"><span class="icon-next"></span></a> -</div> -{% endblock %} diff --git a/nikola/data/themes/bootstrap3/README.md b/nikola/data/themes/bootstrap3/README.md deleted file mode 100644 index 10e673a..0000000 --- a/nikola/data/themes/bootstrap3/README.md +++ /dev/null @@ -1,8 +0,0 @@ -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 -developers at least). This one uses the default bootstrap3 flat look. - -This theme supports Bootswtach font/color schemes (unlike -bootstrap3-gradients) through the `nikola bootswatch_theme` command. diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css deleted file mode 120000 index 78d39af..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map deleted file mode 120000 index 639bdc1..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css deleted file mode 120000 index 200c765..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map deleted file mode 120000 index fcd3722..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap.css deleted file mode 120000 index 013623e..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map deleted file mode 120000 index 8448a3d..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css deleted file mode 120000 index 5bc6076..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map deleted file mode 120000 index 5914aca..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/colorbox.css b/nikola/data/themes/bootstrap3/assets/css/colorbox.css deleted file mode 120000 index 5f8b3b0..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/colorbox.css +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/example3/colorbox.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/docs.css b/nikola/data/themes/bootstrap3/assets/css/docs.css deleted file mode 100644 index 189ea89..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/docs.css +++ /dev/null @@ -1,160 +0,0 @@ -body { - font-weight: 300; -} - -a:hover, -a:focus { - text-decoration: none; -} - -.container { - max-width: 700px; -} - -h2 { - text-align: center; - font-weight: 300; -} - - -/* Header --------------------------------------------------- */ - -.jumbotron { - position: relative; - font-size: 16px; - color: #fff; - color: rgba(255,255,255,.75); - text-align: center; - background-color: #b94a48; - border-radius: 0; -} -.jumbotron h1, -.jumbotron .glyphicon-ok { - margin-bottom: 15px; - font-weight: 300; - letter-spacing: -1px; - color: #fff; -} -.jumbotron .glyphicon-ok { - font-size: 40px; - line-height: 1; -} -.btn-outline { - margin-top: 15px; - margin-bottom: 15px; - padding: 18px 24px; - font-size: inherit; - font-weight: 500; - color: #fff; /* redeclare to override the `.jumbotron a` */ - background-color: transparent; - border-color: #fff; - border-color: rgba(255,255,255,.5); - transition: all .1s ease-in-out; -} -.btn-outline:hover, -.btn-outline:active { - color: #b94a48; - background-color: #fff; - border-color: #fff; -} - -.jumbotron:after { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 10; - display: block; - content: ""; - height: 30px; - background-image: -moz-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1)); - background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1)); -} - -.jumbotron p a, -.jumbotron-links a { - font-weight: 500; - color: #fff; - transition: all .1s ease-in-out; -} -.jumbotron p a:hover, -.jumbotron-links a:hover { - text-shadow: 0 0 10px rgba(255,255,255,.55); -} - -/* Textual links */ -.jumbotron-links { - margin-top: 15px; - margin-bottom: 0; - padding-left: 0; - list-style: none; - font-size: 14px; -} -.jumbotron-links li { - display: inline; -} -.jumbotron-links li + li { - margin-left: 20px; -} - -@media (min-width: 768px) { - .jumbotron { - padding-top: 100px; - padding-bottom: 100px; - font-size: 21px; - } - .jumbotron h1, - .jumbotron .glyphicon-ok { - font-size: 50px; - } -} - -/* Steps for setup --------------------------------------------------- */ - -.how-to { - padding: 50px 20px; - border-top: 1px solid #eee; -} -.how-to li { - font-size: 21px; - line-height: 1.5; - margin-top: 20px; -} -.how-to li p { - font-size: 16px; - color: #555; -} -.how-to code { - font-size: 85%; - color: #b94a48; - background-color: #fcf3f2; - word-wrap: break-word; - white-space: normal; -} - -/* Icons --------------------------------------------------- */ - -.the-icons { - padding: 40px 10px; - font-size: 20px; - line-height: 2; - color: #333; - text-align: center; -} -.the-icons .glyphicon { - padding-left: 15px; - padding-right: 15px; -} - -/* Footer --------------------------------------------------- */ - -.footer { - padding: 50px 30px; - color: #777; - text-align: center; - border-top: 1px solid #eee; -} diff --git a/nikola/data/themes/bootstrap3/assets/css/images/controls.png b/nikola/data/themes/bootstrap3/assets/css/images/controls.png deleted file mode 120000 index 841a726..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/controls.png +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/example3/images/controls.png
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png Binary files differdeleted file mode 100644 index 0d4475e..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png Binary files differdeleted file mode 100644 index 2775eba..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png Binary files differdeleted file mode 100644 index f7f5137..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png Binary files differdeleted file mode 100644 index a2d63d1..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png Binary files differdeleted file mode 100644 index fd7c3e8..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png Binary files differdeleted file mode 100644 index 2937a9c..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png Binary files differdeleted file mode 100644 index f9d458b..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png Binary files differdeleted file mode 100644 index 74b8583..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png +++ /dev/null diff --git a/nikola/data/themes/bootstrap3/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3/assets/css/images/loading.gif deleted file mode 120000 index b192a75..0000000 --- a/nikola/data/themes/bootstrap3/assets/css/images/loading.gif +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/example3/images/loading.gif
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot deleted file mode 120000 index c2dfd17..0000000 --- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg deleted file mode 120000 index 30abe9d..0000000 --- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf deleted file mode 120000 index 93e3bf3..0000000 --- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff deleted file mode 120000 index f7595ae..0000000 --- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 deleted file mode 120000 index 8c1e4d3..0000000 --- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/bootstrap.js b/nikola/data/themes/bootstrap3/assets/js/bootstrap.js deleted file mode 120000 index 26aa1fd..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/bootstrap.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/js/bootstrap.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js deleted file mode 120000 index c4cdf6c..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/bootstrap/dist/js/bootstrap.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js deleted file mode 120000 index f83073f..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js deleted file mode 120000 index bafc4e0..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js deleted file mode 120000 index 9b995d8..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js deleted file mode 120000 index a749232..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js deleted file mode 120000 index e4a595c..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js deleted file mode 120000 index 1e9a1d6..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js deleted file mode 120000 index 748f53b..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js deleted file mode 120000 index 1154fb5..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js deleted file mode 120000 index 483e192..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js deleted file mode 120000 index a30b13c..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js deleted file mode 120000 index 2a7e8ad..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js deleted file mode 120000 index e359290..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js deleted file mode 120000 index 04fa276..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js deleted file mode 120000 index d8105ab..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js deleted file mode 120000 index 72dddf5..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js deleted file mode 120000 index 34aa3c0..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js deleted file mode 120000 index a87f03c..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js deleted file mode 120000 index 31053b8..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js deleted file mode 120000 index aad9d22..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js deleted file mode 120000 index 3ea27c2..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js deleted file mode 120000 index 3e23b4a..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js deleted file mode 120000 index 374b9bb..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js deleted file mode 120000 index 101b476..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js deleted file mode 120000 index 8e14f15..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js deleted file mode 120000 index 2d03d48..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js deleted file mode 120000 index 9af0ba7..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js deleted file mode 120000 index 34f8ab1..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js deleted file mode 120000 index e20bd38..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js deleted file mode 120000 index 555f2e6..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js deleted file mode 120000 index bac4855..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js deleted file mode 120000 index 65b0492..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js deleted file mode 120000 index 99859fd..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js deleted file mode 120000 index c4fd9d5..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js deleted file mode 120000 index d7f26e0..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js deleted file mode 120000 index 86fd98f..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js deleted file mode 120000 index 7cd1336..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js deleted file mode 120000 index e6c5965..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js deleted file mode 120000 index bd2254c..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js deleted file mode 100644 index 732fa3d..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Flowr.js - Simple jQuery plugin to emulate Flickr's justified view - * For usage information refer to http://github.com/kalyan02/flowr-js - * - * - * @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net) - * @version: v0.1 - */ -(function($) { - //$("#container2").css( 'border', '1px solid #ccc'); - $.fn.flowr = function(options) { - - $this = this; - var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy - var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed - var NO_COPY_FIELDS = ['complete', 'data', 'responsive']; // these attributes will not be carried forward for append related calls - var DEFAULTS = { - 'data': [], - 'padding': 5, // whats the padding between flowy items - 'height': 240, // Minimum height an image row should take - 'render': null, // callback function to get the tag - 'append': false, // TODO - 'widthAttr': 'width', // a custom data structure can specify which attribute refers to height/width - 'heightAttr': 'height', - 'maxScale': 1.5, // In case there is only 1 elment in last row - 'maxWidth': this.width() - 1, // 1px is just for offset - 'itemWidth': null, // callback function for width - 'itemHeight': null, // callback function for height - 'complete': null, // complete callback - 'rowClassName': ROW_CLASS_NAME, - 'rows': -1, // Maximum number of rows to render. -1 for no limit. - 'responsive': true // make content responsive - }; - var settings = $.extend(DEFAULTS, options); - - // If data is being appended, we already have settings - // If we already have settings, retrieve them - if (settings.append && $this.data('lastSettings')) { - lastSettings = $this.data('lastSettings'); - - // Copy over the settings from previous init - for (attr in DEFAULTS) { - if (NO_COPY_FIELDS.indexOf(attr) < 0 && settings[attr] == DEFAULTS[attr]) { - settings[attr] = lastSettings[attr]; - } - } - - // Check if we have an incomplete last row - lastRow = $this.data('lastRow'); - if (lastRow.data.length > 0 && settings.maxWidth - lastRow.width > MAX_LAST_ROW_GAP) { - // Prepend the incomplete row to newly loaded data and redraw - lastRowData = lastSettings.data.slice(lastSettings.data.length - lastRow.data.length - 1); - settings.data = lastRowData.concat(settings.data); - - // Remove the incomplete row - // TODO: Don't reload this stuff later. Reattach to new row. - $('.' + settings.rowClassName + ':last', $this).detach(); - } else { - // console.log( lastRow.data.length ); - // console.log( lastRow.width ); - } - } - - // only on the first initial call - if (!settings.responsive && !settings.append) - $this.width($this.width()); - - // Basic sanity checks - if (!(settings.data instanceof Array)) - return; - - if (typeof(settings.padding) != 'number') - settings.padding = parseInt(settings.padding); - - if (typeof(settings.itemWidth) != 'function') { - settings.itemWidth = function(data) { - return data[settings.widthAttr]; - } - } - - if (typeof(settings.itemHeight) != 'function') { - settings.itemHeight = function(data) { - return data[settings.heightAttr]; - } - } - - // A standalone utility to calculate the item widths for a particular row - // Returns rowWidth: width occupied & data : the items in the new row - var utils = { - getNextRow: function(data, settings) { - var itemIndex = 0; - var itemsLength = data.length; - var lineItems = []; - var lineWidth = 0; - var maxWidth = settings.maxWidth; - var paddingSize = settings.padding; - - // console.log( 'maxItems=' + data.length ); - - requiredPadding = function() { - var extraPads = arguments.length == 1 ? arguments[0] : 0; - return (lineItems.length - 1 + extraPads) * settings.padding; - } - - while (lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength)) { - var itemData = data[itemIndex]; - var itemWidth = settings.itemWidth.call($this, itemData); - var itemHeight = settings.itemHeight.call($this, itemData); - - var minHeight = settings.height; - var minWidth = Math.floor(itemWidth * settings.height / itemHeight); - - - if (minWidth > settings.maxWidth) { - // very short+wide images like panoramas - // show them even if ugly, as wide as possible - minWidth = settings.maxWidth - 1 - requiredPadding(1); - minHeight = settings.height * minHeight / minWidth; - } - var newLineWidth = lineWidth + minWidth; - - // console.log( 'lineWidth = ' + lineWidth ); - // console.log( 'newLineWidth = ' + newLineWidth ); - if (newLineWidth < settings.maxWidth) { - lineItems.push({ - 'height': minHeight, - 'width': minWidth, - 'itemData': itemData - }); - - lineWidth += minWidth; - itemIndex++; - } else { - // We'd have exceeded width. So break off to scale. - // console.log( 'breaking off = ' + itemIndex ); - // console.log( 'leave off size = ' + lineItems.length ); - break; - } - } //while - - // Scale the size to max width - testWidth = 0; - if (lineWidth < settings.maxWidth) { - var fullScaleWidth = settings.maxWidth - requiredPadding() - 10; - var currScaleWidth = lineWidth; - var scaleFactor = fullScaleWidth / currScaleWidth; - if (scaleFactor > settings.maxScale) - scaleFactor = 1; - - var newHeight = Math.round(settings.height * scaleFactor); - for (i = 0; i < lineItems.length; i++) { - var lineItem = lineItems[i]; - lineItem.width = Math.floor(lineItem.width * scaleFactor); - lineItem.height = newHeight; - - testWidth += lineItem.width; - } - } - - return { - data: lineItems, - width: testWidth + requiredPadding() - }; - }, //getNextRow - reorderContent: function() { - /* - TODO: optimize for faster resizing by reusing dom objects instead of killing the dom - */ - var _initialWidth = $this.data('width'); - var _newWidth = $this.width(); - var _change = _initialWidth - _newWidth; - - if (_initialWidth != _newWidth) { - $this.html(''); - var _settings = $this.data('lastSettings'); - _settings.data = $this.data('data'); - _settings.maxWidth = $this.width() - 1; - $this.flowr(_settings); - } - } - } //utils - - // If the responsive var is set to true then listen for resize method - // and prevent resizing from happening twice if responsive is set again during append phase! - if (settings.responsive && !$this.data('__responsive')) { - $(window).resize(function() { - initialWidth = $this.data('width'); - newWidth = $this.width(); - - //initiate resize - if (initialWidth != newWidth) { - var task_id = $this.data('task_id'); - if (task_id) { - task_id = clearTimeout(task_id); - task_id = null; - } - task_id = setTimeout(utils.reorderContent, 80); - $this.data('task_id', task_id); - } - }); - $this.data('__responsive', true); - } - - - return this.each(function() { - - // Get a copy of original data. 1 level deep copy is sufficient. - var data = settings.data.slice(0); - var rowData = null; - var currentRow = 0; - var currentItem = 0; - - // Store all the data - var allData = []; - for (i = 0; i < data.length; i++) { - allData.push(data[i]); - } - $this.data('data', allData); - - // While we have a new row - while ((rowData = utils.getNextRow(data, settings)) != null && rowData.data.length > 0) { - if (settings.rows > 0 && currentRow >= settings.rows) - break; - // remove the number of elements in the new row from the top of data stack - data.splice(0, rowData.data.length); - - // Create a new row div, add class, append the htmls and insert the flowy items - var $row = $('<div>').addClass(settings.rowClassName); - var slack = $this[0].clientWidth - rowData.width - 2 * settings.padding - for (i = 0; i < rowData.data.length; i++) { - var displayData = rowData.data[i]; - // Get the HTML object from custom render function passed as argument - var displayObject = settings.render.call($this, displayData); - displayObject = $(displayObject); - extraw = Math.floor(slack/rowData.data.length) - if (i == 0) { - extraw += slack % rowData.data.length - } - // Set some basic stuff - displayObject - .css('width', displayData.width + extraw) - .css('height', displayData.height) - .css('margin-bottom', settings.padding + "px") - .css('margin-left', i == 0 ? '0' : settings.padding + "px"); //TODO:Refactor - $row.append(displayObject); - - currentItem++; - } - $this.append($row); - // console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width ); - - currentRow++; - $this.data('lastRow', rowData); - } - // store the current state of settings and the items in last row - // we'll need this info when we append more items - $this.data('lastSettings', settings); - - // onComplete callback - // pass back info about list of rows and items rendered - if (typeof(settings.complete) == 'function') { - var completeData = { - renderedRows: currentRow, - renderedItems: currentItem - } - settings.complete.call($this, completeData); - } - }); - }; - -})(jQuery); diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js deleted file mode 120000 index 9e40fd4..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/jquery.colorbox-min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js deleted file mode 120000 index 5ee7a90..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.js b/nikola/data/themes/bootstrap3/assets/js/jquery.js deleted file mode 120000 index 966173b..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/jquery.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.min.js b/nikola/data/themes/bootstrap3/assets/js/jquery.min.js deleted file mode 120000 index 5c080da..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/jquery.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.min.map b/nikola/data/themes/bootstrap3/assets/js/jquery.min.map deleted file mode 120000 index 7e2c217..0000000 --- a/nikola/data/themes/bootstrap3/assets/js/jquery.min.map +++ /dev/null @@ -1 +0,0 @@ -../../../../../../bower_components/jquery/dist/jquery.min.map
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/bundles b/nikola/data/themes/bootstrap3/bundles deleted file mode 100644 index 8bdc591..0000000 --- a/nikola/data/themes/bootstrap3/bundles +++ /dev/null @@ -1,4 +0,0 @@ -assets/css/all-nocdn.css=bootstrap.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/bootstrap3/engine b/nikola/data/themes/bootstrap3/engine deleted file mode 100644 index 2951cdd..0000000 --- a/nikola/data/themes/bootstrap3/engine +++ /dev/null @@ -1 +0,0 @@ -mako diff --git a/nikola/data/themes/bootstrap3/parent b/nikola/data/themes/bootstrap3/parent deleted file mode 100644 index df967b9..0000000 --- a/nikola/data/themes/bootstrap3/parent +++ /dev/null @@ -1 +0,0 @@ -base diff --git a/nikola/data/themes/bootstrap3/templates/base.tmpl b/nikola/data/themes/bootstrap3/templates/base.tmpl deleted file mode 100644 index 2f7a290..0000000 --- a/nikola/data/themes/bootstrap3/templates/base.tmpl +++ /dev/null @@ -1,94 +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 --> - -<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 collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false"> - <span class="sr-only">${messages("Toggle navigation")}</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a class="navbar-brand" href="${abs_link(_link("root", None, lang))}"> - %if logo_url: - <img src="${logo_url}" alt="${blog_title|h}" id="logo"> - %endif - - % if show_blog_title: - <span id="blog-title">${blog_title|h}</span> - % endif - </a> - </div><!-- /.navbar-header --> - <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false"> - <ul class="nav navbar-nav"> - ${base.html_navigation_links()} - ${template_hooks['menu']()} - </ul> - %if search_form: - ${search_form} - %endif - - <ul class="nav navbar-nav navbar-right"> - <%block name="belowtitle"> - %if len(translations) > 1: - <li>${base.html_translations()}</li> - %endif - </%block> - % if show_sourcelink: - <%block name="sourcelink"></%block> - %endif - ${template_hooks['menu_alt']()} - </ul> - </div><!-- /.navbar-collapse --> - </div><!-- /.container --> -</nav> - -<!-- End of Menubar --> - -<div class="container" id="content" role="main"> - <div class="body-content"> - <!--Body content--> - <div class="row"> - ${template_hooks['page_header']()} - <%block name="content"></%block> - </div> - <!--End of body content--> - - <footer id="footer"> - ${content_footer} - ${template_hooks['page_footer']()} - </footer> - </div> -</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/bootstrap3/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl deleted file mode 100644 index 20b135b..0000000 --- a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl +++ /dev/null @@ -1,188 +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 use_base_tag: - <base href="${abs_link(permalink)}"> - % endif - %if description: - <meta name="description" content="${description|h}"> - %endif - <meta name="viewport" content="width=device-width, initial-scale=1"> - %if title == blog_title: - <title>${blog_title|h}</title> - %else: - <title>${title|h} | ${blog_title|h}</title> - %endif - - ${html_stylesheets()} - <meta content="${theme_color}" name="theme-color"> - ${html_feedlinks()} - <link rel="canonical" href="${abs_link(permalink)}"> - - %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="https://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="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> - - <script src="/assets/js/all.js"></script> - %else: - <script src="/assets/js/all-nocdn.js"></script> - %endif - %else: - %if use_cdn: - <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - <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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - %else: - <link href="/assets/css/bootstrap.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" 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) == "#": - <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 sorted(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 sorted(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 sorted(translations): - %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/bootstrap3/templates/gallery.tmpl b/nikola/data/themes/bootstrap3/templates/gallery.tmpl deleted file mode 100644 index 3dbfa82..0000000 --- a/nikola/data/themes/bootstrap3/templates/gallery.tmpl +++ /dev/null @@ -1,95 +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="glyphicon glyphicon-folder-open"></i> ${ftitle|h}</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']|h}"> - <img src="${image['url_thumb']}" alt="${image['title']|h}" /></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}); -$('a.image-reference[href="'+window.location.hash.substring(1,1000)+'"]').click(); -</script> -</%block> diff --git a/nikola/data/themes/bootstrap3/templates/slides.tmpl b/nikola/data/themes/bootstrap3/templates/slides.tmpl deleted file mode 100644 index a73848a..0000000 --- a/nikola/data/themes/bootstrap3/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"><span class="icon-prev"></span></a> - <a class="right carousel-control" href="#${carousel_id}" data-slide="next"><span class="icon-next"></span></a> -</div> -</%block> diff --git a/nikola/data/themes/bootstrap4-jinja/README.md b/nikola/data/themes/bootstrap4-jinja/README.md new file mode 100644 index 0000000..bb1b484 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/README.md @@ -0,0 +1,10 @@ +This is a theme based on Bootstrap 4. + +The theme is a good building block for a site. It is based on a simple navbar + +content layout. For a more blog-style layout, check out `bootblog4`. + +Note that unlike previous versions of Bootstrap, icon fonts are not built-in. +You can use Font Awesome for this. + +This theme supports Bootswatch font/color schemes through the `nikola +bootwatch_theme` command. diff --git a/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css new file mode 120000 index 0000000..8c8dc62 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/bootstrap/dist/css/bootstrap.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap4-jinja/assets/css/theme.css index 52466de..20eee8e 100644 --- a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css +++ b/nikola/data/themes/bootstrap4-jinja/assets/css/theme.css @@ -1,29 +1,11 @@ -#container { - width: 960px; - margin: 0 auto; -} - -#contentcolumn { - max-width: 760px; -} -#q { - width: 150px; -} - img { - max-width: 90%; -} - -.postbox { - border-bottom: 2px solid darkgrey; - margin-bottom: 12px; + max-width: 100%; } .titlebox { text-align: right; } -#addthisbox {margin-bottom: 12px;} td.label { /* Issue #290 */ @@ -36,7 +18,6 @@ td.label { font-size: xx-small; } - .caption { /* Issue 292 */ text-align: center; @@ -52,7 +33,7 @@ div.figure > a > img { } blockquote p, blockquote { - font-size: 17.5px; + font-size: 1.25rem; font-weight: 300; line-height: 1.25; } @@ -67,10 +48,6 @@ ul.bricks > li { margin: 3px; } -.at300b, .stMainServices, .stButton, .stButton_gradient { - box-sizing: content-box; -} - pre, pre code { white-space: pre; word-wrap: normal; @@ -86,10 +63,6 @@ article.post-micro { display: inline-block; } -.flowr_row { - width: 100%; -} - .tags { padding-left: 0; margin-left: -5px; @@ -100,21 +73,25 @@ article.post-micro { .tags > li { display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; +} +.tags > li a { + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; - background-color: #999; - border-radius: 10px; + border-radius: .25rem; + background-color: #868e96; } -.tags > li a { +.tags > li a:hover { color: #fff; + text-decoration: none; + background-color: #6c757d; } .metadata p:before, @@ -144,27 +121,6 @@ article.post-micro { margin-top: 1em; } -.navbar-brand { - padding: 0 15px; -} - -.navbar-brand #blog-title { - padding: 15px 0; - display: inline-block; -} - -.navbar-brand #logo { - max-width: 100%; -} - -.navbar-brand>img { - display: inline; -} - -.row { - margin: 0; -} - /* for alignment with Bootstrap's .entry-content styling */ .entry-summary { margin-top: 1em; @@ -177,14 +133,6 @@ article.post-micro { border-top: 1px solid #e5e5e5; } -.codetable { - table-layout: fixed; -} - -.codetable pre { - overflow-x: scroll; -} - /* hat tip bootstrap/html5 boilerplate */ @media print { *, *:before, *:after { @@ -213,3 +161,72 @@ article.post-micro { display: none; } } + +pre, .codetable { + border: 1px solid #ccc; + border-radius: 0.25rem; + margin-bottom: 1rem; +} + +pre { + padding: 0.75rem; +} + +.codetable tr:first-child td.linenos { + border-top-left-radius: 0.25rem; +} + +.codetable tr:last-child td.linenos { + border-bottom-left-radius: 0.25rem; +} + +.postindexpager { + padding-bottom: 1rem; +} + +ul.navbar-nav { + margin-top: 0; +} + +ul.pager { + display: flex; + padding-left: 0; + list-style: none; + border-radius: .25rem; + padding-left: 0; + margin: 0.5rem 0; +} + +ul.pager li.previous { + margin-right: auto; + display: inline; +} + +ul.pager li.next { + margin-left: auto; + display: inline; +} + + +ul.pager li a { + display: inline; + position: relative; + padding: .5rem .75rem; + margin-left: -1px; + line-height: 1.25; + border: 1px solid #ddd; + border-radius: .25rem; +} + +pre.code { + white-space: pre-wrap; +} + +.byline a:not(:last-child):after { + content: ","; +} + +/* Override incorrect Bootstrap 4 default */ +html[dir="rtl"] body { + text-align: right; +} diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js new file mode 120000 index 0000000..593bffb --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/bootstrap/dist/js/bootstrap.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js new file mode 120000 index 0000000..2a592f6 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/jquery/dist/jquery.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js new file mode 120000 index 0000000..43fca04 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/popper.js/dist/umd/popper.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme b/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme new file mode 100644 index 0000000..be27ebc --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme @@ -0,0 +1,12 @@ +[Theme] +engine = jinja +parent = base-jinja +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT +based_on = Bootstrap 4 <http://getbootstrap.com/> +tags = bootstrap + +[Family] +family = bootstrap4 +mako-version = bootstrap4 diff --git a/nikola/data/themes/bootstrap4-jinja/bundles b/nikola/data/themes/bootstrap4-jinja/bundles new file mode 120000 index 0000000..500c93e --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/bundles @@ -0,0 +1 @@ +../bootstrap4/bundles
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3-jinja/templates/authors.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/authors.tmpl index d65c727..922de74 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/authors.tmpl +++ b/nikola/data/themes/bootstrap4-jinja/templates/authors.tmpl @@ -1,9 +1,17 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% block extra_head %} + {{ feeds_translations.head(kind=kind, feeds=False) }} +{% endblock %} {% block content %} {% if items %} <h2>{{ messages("Authors") }}</h2> + <div class="metadata"> + {{ feeds_translations.translation_link(kind) }} + </div> {% endif %} {% if items %} <ul class="list-inline"> diff --git a/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl new file mode 100644 index 0000000..0748bb2 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl @@ -0,0 +1,105 @@ +{# -*- 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 --> + +<nav class="navbar navbar-expand-md static-top mb-4 +{% if theme_config.get('navbar_light') %} +navbar-light +{% else %} +navbar-dark +{% endif %} +{% if theme_config.get('navbar_custom_bg') %} +{{ theme_config['navbar_custom_bg'] }} +{% elif theme_config.get('navbar_light') %} +bg-light +{% else %} +bg-dark +{% endif %} +"> + <div class="container"><!-- This keeps the margins nice --> + <a class="navbar-brand" href="{{ _link("root", None, lang) }}"> + {% if logo_url %} + <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo" class="d-inline-block align-top"> + {% endif %} + + {% if show_blog_title %} + <span id="blog-title">{{ blog_title|e }}</span> + {% endif %} + </a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="bs-navbar"> + <ul class="navbar-nav mr-auto"> + {{ base.html_navigation_links_entries(navigation_links) }} + {{ template_hooks['menu']() }} + </ul> + {% if search_form %} + {{ search_form }} + {% endif %} + + <ul class="navbar-nav navbar-right"> + {{ base.html_navigation_links_entries(navigation_alt_links) }} + {% block belowtitle %} + {% if translations|length > 1 %} + <li>{{ base.html_translations() }}</li> + {% endif %} + {% endblock %} + {% if show_sourcelink %} + {% block sourcelink %}{% endblock %} + {% endif %} + {{ template_hooks['menu_alt']() }} + </ul> + </div><!-- /.navbar-collapse --> + </div><!-- /.container --> +</nav> + +<!-- End of Menubar --> + +<div class="container" id="content" role="main"> + <div class="body-content"> + <!--Body content--> + {{ template_hooks['page_header']() }} + {% block extra_header %}{% endblock %} + {% block content %}{% endblock %} + <!--End of body content--> + + <footer id="footer"> + {{ content_footer }} + {{ template_hooks['page_footer']() }} + {% block extra_footer %}{% endblock %} + </footer> + </div> +</div> + +{{ base.late_load_js() }} + {% if date_fanciness != 0 %} + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}"; + fancydates({{ date_fanciness }}, {{ luxon_date_format }}); + </script> + <!-- end fancy dates --> + {% endif %} + {% block extra_js %}{% endblock %} + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> +{{ body_end }} +{{ template_hooks['body_end']() }} +</body> +</html> diff --git a/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl new file mode 100644 index 0000000..b4bcf85 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl @@ -0,0 +1,165 @@ +{# -*- coding: utf-8 -*- #} +{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %} + +{% macro html_headstart() %} +<!DOCTYPE html> +<html + +prefix=' +og: http://ogp.me/ns# article: http://ogp.me/ns/article# +{% if comment_system == 'facebook' %} +fb: http://ogp.me/ns/fb# +{% endif %} +' +{% if is_rtl %} +dir="rtl" +{% endif %} + +lang="{{ lang }}"> + <head> + <meta charset="utf-8"> + {% if description %} + <meta name="description" content="{{ description|e }}"> + {% endif %} + <meta name="viewport" content="width=device-width, initial-scale=1"> + {% if title == blog_title %} + <title>{{ blog_title|e }}</title> + {% else %} + <title>{{ title|e }} | {{ blog_title|e }}</title> + {% endif %} + + {{ html_stylesheets() }} + <meta name="theme-color" content="{{ theme_color }}"> + {% if meta_generator_tag %} + <meta name="generator" content="Nikola (getnikola.com)"> + {% endif %} + {{ html_feedlinks() }} + <link rel="canonical" href="{{ abs_link(permalink) }}"> + + {% 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 %} + + {% if use_cdn %} + <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + {% else %} + <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang, url_type) }}"></script><![endif]--> + {% endif %} + + {{ extra_head_data }} +{% endmacro %} + +{% macro late_load_js() %} + {% if use_cdn %} + <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + {% endif %} + {% if use_bundles and use_cdn %} + <script src="/assets/js/all.js"></script> + {% elif use_bundles %} + <script src="/assets/js/all-nocdn.js"></script> + {% else %} + {% if not use_cdn %} + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/popper.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + <script src="/assets/js/baguetteBox.min.js"></script> + {% endif %} + {% endif %} + {% if date_fanciness != 0 %} + {% if date_fanciness == 2 %} + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script> + {% endif %} + {% if use_cdn %} + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + {% else %} + <script src="/assets/js/luxon.min.js"></script> + {% endif %} + {% if not use_bundles %} + <script src="/assets/js/fancydates.min.js"></script> + {% endif %} + {% endif %} + {{ social_buttons_code }} +{% endmacro %} + + +{% macro html_stylesheets() %} + {% if use_cdn %} + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> + {% endif %} + {% if use_bundles and use_cdn %} + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + {% elif use_bundles %} + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + {% else %} + {% if not use_cdn %} + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.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/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 %} +{% endmacro %} + +{% macro html_navigation_links() %} + {{ html_navigation_links_entries(navigation_links) }} +{% endmacro %} + +{% macro html_navigation_links_entries(navigation_links_source) %} + {% for url, text in navigation_links_source[lang] %} + {% if isinstance(url, tuple) %} + <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }}</a> + <div class="dropdown-menu"> + {% for suburl, text in url %} + {% if rel_link(permalink, suburl) == "#" %} + <a href="{{ permalink }}" class="dropdown-item active">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a> + {% else %} + <a href="{{ suburl }}" class="dropdown-item">{{ text }}</a> + {% endif %} + {% endfor %} + </div> + {% else %} + {% if rel_link(permalink, url) == "#" %} + <li class="nav-item active"><a href="{{ permalink }}" class="nav-link">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a> + {% else %} + <li class="nav-item"><a href="{{ url }}" class="nav-link">{{ text }}</a> + {% endif %} + {% endif %} + {% endfor %} +{% endmacro %} + +{% macro html_feedlinks() %} + {{ feeds_translations.head(classification=None, kind='index', other=False) }} +{% endmacro %} + +{% macro html_translations() %} + {% for langname in translations|sort %} + {% if langname != lang %} + <li class="nav-item"><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}" class="nav-link">{{ messages("LANGUAGE", langname) }}</a></li> + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl new file mode 100644 index 0000000..2fec2c6 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl @@ -0,0 +1,13 @@ +{# -*- coding: utf-8 -*- #} +{% macro html_pager() %} + {% if prevlink or nextlink %} + <ul class="pager postindexpager clearfix"> + {% if prevlink %} + <li class="previous"><a href="{{ prevlink }}" rel="prev">{{ messages("Newer posts") }}</a></li> + {% endif %} + {% if nextlink %} + <li class="next"><a href="{{ nextlink }}" rel="next">{{ messages("Older posts") }}</a></li> + {% endif %} + </ul> + {% endif %} +{% endmacro %} diff --git a/nikola/data/themes/bootstrap3-jinja/templates/listing.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/listing.tmpl index ed99410..56a1b4f 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/listing.tmpl +++ b/nikola/data/themes/bootstrap4-jinja/templates/listing.tmpl @@ -1,15 +1,15 @@ {# -*- coding: utf-8 -*- #} {% extends 'base.tmpl' %} -{% import 'crumbs.tmpl' as ui with context %} +{% import 'ui_helper.tmpl' as ui with context %} {% block content %} -{{ ui.bar(crumbs) }} +{{ ui.breadcrumbs(crumbs) }} {% if folders or files %} <ul> {% for name in folders %} - <li><a href="{{ name|urlencode }}"><i class="glyphicon glyphicon-folder-open"></i> {{ name|e }}</a> + <li><a href="{{ name|e }}">📂 {{ name|e }}</a> {% endfor %} {% for name in files %} - <li><a href="{{ name|urlencode }}.html"><i class="glyphicon glyphicon-file"></i> {{ name|e }}</a> + <li><a href="{{ name|e }}.html">📄 {{ name|e }}</a> {% endfor %} </ul> {% endif %} @@ -24,9 +24,7 @@ {% endblock %} {% block sourcelink %} -{% if source_link %} - <li> - <a href="{{ source_link }}" id="sourcelink">{{ messages("Source") }}</a> - </li> +{% if source_link and show_sourcelink %} + {{ ui.show_sourcelink(source_link) }} {% endif %} {% endblock %} diff --git a/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl new file mode 100644 index 0000000..30fe534 --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl @@ -0,0 +1,40 @@ +{# -*- coding: utf-8 -*- #} +{% macro page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5) %} +<nav aria-label="Page navigation"> + <ul class="pagination"> + {% if prev_next_links_reversed %} + {% if nextlink %} + <li class="page-item"><a href="{{ nextlink }}" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">«</span></a></li> + {% else %} + <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">«</span></a></li> + {% endif %} + {% else %} + {% if prevlink %} + <li class="page-item"><a href="{{ prevlink }}" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">«</span></a></li> + {% else %} + <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">«</span></a></li> + {% endif %} + {% endif %} + {% for i, link in enumerate(page_links) %} + {% if (i - current_page)|abs <= surrounding or i == 0 or i == page_links|length - 1 %} + <li class="page-item {{ 'active' if i == current_page else '' }}"><a href="{{ link }}" class="page-link">{{ i + 1 }}{{ ' <span class="sr-only">(current)</span>' if i == current_page else '' }}</a></li> + {% elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1 %} + <li class="page-item disabled"><a href="#" class="page-link" aria-label="…"><span aria-hidden="true">…</span></a></li> + {% endif %} + {% endfor %} + {% if prev_next_links_reversed %} + {% if prevlink %} + <li class="page-item"><a href="{{ prevlink }}" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">»</span></a></li> + {% else %} + <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">»</span></a></li> + {% endif %} + {% else %} + {% if nextlink %} + <li class="page-item"><a href="{{ nextlink }}" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">»</span></a></li> + {% else %} + <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">»</span></a></li> + {% endif %} + {% endif %} + </ul> +</nav> +{% endmacro %} diff --git a/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/post.tmpl index 3cf4c4b..7e18f90 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl +++ b/nikola/data/themes/bootstrap4-jinja/templates/post.tmpl @@ -2,15 +2,14 @@ {% import 'post_helper.tmpl' as helper with context %} {% import 'post_header.tmpl' as pheader with context %} {% import 'comments_helper.tmpl' as comments with context %} +{% import 'math_helper.tmpl' as math with context %} +{% import 'ui_helper.tmpl' as ui with context %} {% extends 'base.tmpl' %} {% block extra_head %} {{ super() }} {% if post.meta('keywords') %} - <meta name="keywords" content="{{ post.meta('keywords')|e }}"> - {% endif %} - {% if post.description() %} - <meta name="description" itemprop="description" content="{{ post.description()|e }}"> + <meta name="keywords" content="{{ smartjoin(', ', post.meta('keywords'))|e }}"> {% endif %} <meta name="author" content="{{ post.author()|e }}"> {% if post.prev_post %} @@ -25,6 +24,7 @@ {{ helper.open_graph_metadata(post) }} {{ helper.twitter_card_information(post) }} {{ helper.meta_translations(post) }} + {{ math.math_styles_ifpost(post) }} {% endblock %} {% block content %} @@ -45,15 +45,13 @@ {{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }} </section> {% endif %} - {{ helper.mathjax_script(post) }} + {{ math.math_scripts_ifpost(post) }} </article> {{ comments.comment_link_script() }} {% endblock %} {% block sourcelink %} {% if show_sourcelink %} - <li> - <a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a> - </li> + {{ ui.show_sourcelink(post.source_link()) }} {% endif %} {% endblock %} diff --git a/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/tags.tmpl index 4afd4d2..0eadff6 100644 --- a/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl +++ b/nikola/data/themes/bootstrap4-jinja/templates/tags.tmpl @@ -11,7 +11,7 @@ {% for i in range(indent_change_before) %} <ul class="list-inline"> {% endfor %} - <li><a class="reference badge" href="{{ link }}">{{ text|e }}</a> + <li class="list-inline-item"><a class="reference badge badge-secondary" href="{{ link }}">{{ text|e }}</a> {% if indent_change_after <= 0 %} </li> {% endif %} @@ -30,7 +30,7 @@ <ul class="list-inline"> {% for text, link in items %} {% if text not in hidden_tags %} - <li><a class="reference badge" href="{{ link }}">{{ text|e }}</a></li> + <li class="list-inline-item"><a class="reference badge badge-secondary" href="{{ link }}">{{ text|e }}</a></li> {% endif %} {% endfor %} </ul> diff --git a/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl new file mode 100644 index 0000000..d8c8d4d --- /dev/null +++ b/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl @@ -0,0 +1,24 @@ +{# -*- coding: utf-8 -*- #} +{% macro breadcrumbs(crumbs) %} +{% if crumbs %} +<nav class="breadcrumbs"> +<ul class="breadcrumb"> + {% for link, text in crumbs %} + {% if text != index_file %} + {% if link == '#' %} + <li class="breadcrumb-item active">{{ text.rsplit('.html', 1)[0] }}</li> + {% else %} + <li class="breadcrumb-item"><a href="{{ link }}">{{ text }}</a></li> + {% endif %} + {% endif %} + {% endfor %} +</ul> +</nav> +{% endif %} +{% endmacro %} + +{% macro show_sourcelink(sourcelink_href) %} + <li class="nav-item"> + <a href="{{ sourcelink_href }}" id="sourcelink" class="nav-link">{{ messages("Source") }}</a> + </li> +{% endmacro %} diff --git a/nikola/data/themes/bootstrap4/README.md b/nikola/data/themes/bootstrap4/README.md new file mode 100644 index 0000000..bb1b484 --- /dev/null +++ b/nikola/data/themes/bootstrap4/README.md @@ -0,0 +1,10 @@ +This is a theme based on Bootstrap 4. + +The theme is a good building block for a site. It is based on a simple navbar + +content layout. For a more blog-style layout, check out `bootblog4`. + +Note that unlike previous versions of Bootstrap, icon fonts are not built-in. +You can use Font Awesome for this. + +This theme supports Bootswatch font/color schemes through the `nikola +bootwatch_theme` command. diff --git a/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css new file mode 120000 index 0000000..8c8dc62 --- /dev/null +++ b/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/bootstrap/dist/css/bootstrap.min.css
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap3/assets/css/theme.css b/nikola/data/themes/bootstrap4/assets/css/theme.css index 52466de..20eee8e 100644 --- a/nikola/data/themes/bootstrap3/assets/css/theme.css +++ b/nikola/data/themes/bootstrap4/assets/css/theme.css @@ -1,29 +1,11 @@ -#container { - width: 960px; - margin: 0 auto; -} - -#contentcolumn { - max-width: 760px; -} -#q { - width: 150px; -} - img { - max-width: 90%; -} - -.postbox { - border-bottom: 2px solid darkgrey; - margin-bottom: 12px; + max-width: 100%; } .titlebox { text-align: right; } -#addthisbox {margin-bottom: 12px;} td.label { /* Issue #290 */ @@ -36,7 +18,6 @@ td.label { font-size: xx-small; } - .caption { /* Issue 292 */ text-align: center; @@ -52,7 +33,7 @@ div.figure > a > img { } blockquote p, blockquote { - font-size: 17.5px; + font-size: 1.25rem; font-weight: 300; line-height: 1.25; } @@ -67,10 +48,6 @@ ul.bricks > li { margin: 3px; } -.at300b, .stMainServices, .stButton, .stButton_gradient { - box-sizing: content-box; -} - pre, pre code { white-space: pre; word-wrap: normal; @@ -86,10 +63,6 @@ article.post-micro { display: inline-block; } -.flowr_row { - width: 100%; -} - .tags { padding-left: 0; margin-left: -5px; @@ -100,21 +73,25 @@ article.post-micro { .tags > li { display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; +} +.tags > li a { + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; - background-color: #999; - border-radius: 10px; + border-radius: .25rem; + background-color: #868e96; } -.tags > li a { +.tags > li a:hover { color: #fff; + text-decoration: none; + background-color: #6c757d; } .metadata p:before, @@ -144,27 +121,6 @@ article.post-micro { margin-top: 1em; } -.navbar-brand { - padding: 0 15px; -} - -.navbar-brand #blog-title { - padding: 15px 0; - display: inline-block; -} - -.navbar-brand #logo { - max-width: 100%; -} - -.navbar-brand>img { - display: inline; -} - -.row { - margin: 0; -} - /* for alignment with Bootstrap's .entry-content styling */ .entry-summary { margin-top: 1em; @@ -177,14 +133,6 @@ article.post-micro { border-top: 1px solid #e5e5e5; } -.codetable { - table-layout: fixed; -} - -.codetable pre { - overflow-x: scroll; -} - /* hat tip bootstrap/html5 boilerplate */ @media print { *, *:before, *:after { @@ -213,3 +161,72 @@ article.post-micro { display: none; } } + +pre, .codetable { + border: 1px solid #ccc; + border-radius: 0.25rem; + margin-bottom: 1rem; +} + +pre { + padding: 0.75rem; +} + +.codetable tr:first-child td.linenos { + border-top-left-radius: 0.25rem; +} + +.codetable tr:last-child td.linenos { + border-bottom-left-radius: 0.25rem; +} + +.postindexpager { + padding-bottom: 1rem; +} + +ul.navbar-nav { + margin-top: 0; +} + +ul.pager { + display: flex; + padding-left: 0; + list-style: none; + border-radius: .25rem; + padding-left: 0; + margin: 0.5rem 0; +} + +ul.pager li.previous { + margin-right: auto; + display: inline; +} + +ul.pager li.next { + margin-left: auto; + display: inline; +} + + +ul.pager li a { + display: inline; + position: relative; + padding: .5rem .75rem; + margin-left: -1px; + line-height: 1.25; + border: 1px solid #ddd; + border-radius: .25rem; +} + +pre.code { + white-space: pre-wrap; +} + +.byline a:not(:last-child):after { + content: ","; +} + +/* Override incorrect Bootstrap 4 default */ +html[dir="rtl"] body { + text-align: right; +} diff --git a/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js new file mode 120000 index 0000000..593bffb --- /dev/null +++ b/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/bootstrap/dist/js/bootstrap.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4/assets/js/jquery.min.js b/nikola/data/themes/bootstrap4/assets/js/jquery.min.js new file mode 120000 index 0000000..2a592f6 --- /dev/null +++ b/nikola/data/themes/bootstrap4/assets/js/jquery.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/jquery/dist/jquery.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4/assets/js/popper.min.js b/nikola/data/themes/bootstrap4/assets/js/popper.min.js new file mode 120000 index 0000000..43fca04 --- /dev/null +++ b/nikola/data/themes/bootstrap4/assets/js/popper.min.js @@ -0,0 +1 @@ +../../../../../../npm_assets/node_modules/popper.js/dist/umd/popper.min.js
\ No newline at end of file diff --git a/nikola/data/themes/bootstrap4/bootstrap4.theme b/nikola/data/themes/bootstrap4/bootstrap4.theme new file mode 100644 index 0000000..0f3f9ef --- /dev/null +++ b/nikola/data/themes/bootstrap4/bootstrap4.theme @@ -0,0 +1,12 @@ +[Theme] +engine = mako +parent = base +author = The Nikola Contributors +author_url = https://getnikola.com/ +license = MIT +based_on = Bootstrap 4 <http://getbootstrap.com/> +tags = bootstrap + +[Family] +family = bootstrap4 +jinja_version = bootstrap4-jinja diff --git a/nikola/data/themes/bootstrap4/bundles b/nikola/data/themes/bootstrap4/bundles new file mode 100644 index 0000000..71d458b --- /dev/null +++ b/nikola/data/themes/bootstrap4/bundles @@ -0,0 +1,26 @@ +; css bundles +assets/css/all-nocdn.css= + bootstrap.min.css, + rst_base.css, + nikola_rst.css, + code.css, + baguetteBox.min.css, + theme.css, + custom.css, +assets/css/all.css= + rst_base.css, + nikola_rst.css, + code.css, + baguetteBox.min.css, + theme.css, + custom.css, + +; javascript bundles +assets/js/all-nocdn.js= + jquery.min.js, + popper.min.js, + bootstrap.min.js, + baguetteBox.min.js, + fancydates.min.js, +assets/js/all.js= + fancydates.min.js diff --git a/nikola/data/themes/bootstrap3/templates/authors.tmpl b/nikola/data/themes/bootstrap4/templates/authors.tmpl index 2d3bbf5..300377d 100644 --- a/nikola/data/themes/bootstrap3/templates/authors.tmpl +++ b/nikola/data/themes/bootstrap4/templates/authors.tmpl @@ -1,9 +1,17 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%block name="extra_head"> + ${feeds_translations.head(kind=kind, feeds=False)} +</%block> <%block name="content"> % if items: <h2>${messages("Authors")}</h2> + <div class="metadata"> + ${feeds_translations.translation_link(kind)} + </div> % endif % if items: <ul class="list-inline"> diff --git a/nikola/data/themes/bootstrap4/templates/base.tmpl b/nikola/data/themes/bootstrap4/templates/base.tmpl new file mode 100644 index 0000000..21b6141 --- /dev/null +++ b/nikola/data/themes/bootstrap4/templates/base.tmpl @@ -0,0 +1,105 @@ +## -*- 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 --> + +<nav class="navbar navbar-expand-md static-top mb-4 +% if theme_config.get('navbar_light'): +navbar-light +% else: +navbar-dark +% endif +% if theme_config.get('navbar_custom_bg'): +${theme_config['navbar_custom_bg']} +% elif theme_config.get('navbar_light'): +bg-light +% else: +bg-dark +%endif +"> + <div class="container"><!-- This keeps the margins nice --> + <a class="navbar-brand" href="${_link("root", None, lang)}"> + %if logo_url: + <img src="${logo_url}" alt="${blog_title|h}" id="logo" class="d-inline-block align-top"> + %endif + + % if show_blog_title: + <span id="blog-title">${blog_title|h}</span> + % endif + </a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="bs-navbar"> + <ul class="navbar-nav mr-auto"> + ${base.html_navigation_links_entries(navigation_links)} + ${template_hooks['menu']()} + </ul> + %if search_form: + ${search_form} + %endif + + <ul class="navbar-nav navbar-right"> + ${base.html_navigation_links_entries(navigation_alt_links)} + <%block name="belowtitle"> + %if len(translations) > 1: + <li>${base.html_translations()}</li> + %endif + </%block> + % if show_sourcelink: + <%block name="sourcelink"></%block> + %endif + ${template_hooks['menu_alt']()} + </ul> + </div><!-- /.navbar-collapse --> + </div><!-- /.container --> +</nav> + +<!-- End of Menubar --> + +<div class="container" id="content" role="main"> + <div class="body-content"> + <!--Body content--> + ${template_hooks['page_header']()} + <%block name="extra_header"></%block> + <%block name="content"></%block> + <!--End of body content--> + + <footer id="footer"> + ${content_footer} + ${template_hooks['page_footer']()} + <%block name="extra_footer"></%block> + </footer> + </div> +</div> + +${base.late_load_js()} + %if date_fanciness != 0: + <!-- fancy dates --> + <script> + luxon.Settings.defaultLocale = "${luxon_locales[lang]}"; + fancydates(${date_fanciness}, ${luxon_date_format}); + </script> + <!-- end fancy dates --> + %endif + <%block name="extra_js"></%block> + <script> + baguetteBox.run('div#content', { + ignoreClass: 'islink', + captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}}); + </script> +${body_end} +${template_hooks['body_end']()} +</body> +</html> diff --git a/nikola/data/themes/bootstrap4/templates/base_helper.tmpl b/nikola/data/themes/bootstrap4/templates/base_helper.tmpl new file mode 100644 index 0000000..f551116 --- /dev/null +++ b/nikola/data/themes/bootstrap4/templates/base_helper.tmpl @@ -0,0 +1,165 @@ +## -*- coding: utf-8 -*- +<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/> + +<%def name="html_headstart()"> +<!DOCTYPE html> +<html +\ +prefix='\ +og: http://ogp.me/ns# article: http://ogp.me/ns/article# +%if comment_system == 'facebook': +fb: http://ogp.me/ns/fb# \ +%endif +'\ +% if is_rtl: +dir="rtl" \ +% endif +\ +lang="${lang}"> + <head> + <meta charset="utf-8"> + %if description: + <meta name="description" content="${description|h}"> + %endif + <meta name="viewport" content="width=device-width, initial-scale=1"> + %if title == blog_title: + <title>${blog_title|h}</title> + %else: + <title>${title|h} | ${blog_title|h}</title> + %endif + + ${html_stylesheets()} + <meta name="theme-color" content="${theme_color}"> + % if meta_generator_tag: + <meta name="generator" content="Nikola (getnikola.com)"> + % endif + ${html_feedlinks()} + <link rel="canonical" href="${abs_link(permalink)}"> + + %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 + + %if use_cdn: + <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--> + %else: + <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang, url_type)}"></script><![endif]--> + %endif + + ${extra_head_data} +</%def> + +<%def name="late_load_js()"> + %if use_cdn: + <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script> + % endif + %if use_bundles and use_cdn: + <script src="/assets/js/all.js"></script> + %elif use_bundles: + <script src="/assets/js/all-nocdn.js"></script> + %else: + %if not use_cdn: + <script src="/assets/js/jquery.min.js"></script> + <script src="/assets/js/popper.min.js"></script> + <script src="/assets/js/bootstrap.min.js"></script> + <script src="/assets/js/baguetteBox.min.js"></script> + %endif + %endif + %if date_fanciness != 0: + %if date_fanciness == 2: + <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script> + %endif + %if use_cdn: + <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script> + %else: + <script src="/assets/js/luxon.min.js"></script> + %endif + %if not use_bundles: + <script src="/assets/js/fancydates.min.js"></script> + %endif + %endif + ${social_buttons_code} +</%def> + + +<%def name="html_stylesheets()"> + %if use_cdn: + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous"> + % endif + %if use_bundles and use_cdn: + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + %elif use_bundles: + <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css"> + %else: + %if not use_cdn: + <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/baguetteBox.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/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 +</%def> + +<%def name="html_navigation_links()"> + ${html_navigation_links_entries(navigation_links)} +</%def> + +<%def name="html_navigation_links_entries(navigation_links_source)"> + %for url, text in navigation_links_source[lang]: + % if isinstance(url, tuple): + <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text}</a> + <div class="dropdown-menu"> + %for suburl, text in url: + % if rel_link(permalink, suburl) == "#": + <a href="${permalink}" class="dropdown-item active">${text} <span class="sr-only">${messages("(active)", lang)}</span></a> + %else: + <a href="${suburl}" class="dropdown-item">${text}</a> + %endif + %endfor + </div> + % else: + % if rel_link(permalink, url) == "#": + <li class="nav-item active"><a href="${permalink}" class="nav-link">${text} <span class="sr-only">${messages("(active)", lang)}</span></a> + %else: + <li class="nav-item"><a href="${url}" class="nav-link">${text}</a> + %endif + % endif + %endfor +</%def> + +<%def name="html_feedlinks()"> + ${feeds_translations.head(classification=None, kind='index', other=False)} +</%def> + +<%def name="html_translations()"> + %for langname in sorted(translations): + %if langname != lang: + <li class="nav-item"><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}" class="nav-link">${messages("LANGUAGE", langname)}</a></li> + %endif + %endfor +</%def> diff --git a/nikola/data/themes/bootstrap4/templates/index_helper.tmpl b/nikola/data/themes/bootstrap4/templates/index_helper.tmpl new file mode 100644 index 0000000..e6b0089 --- /dev/null +++ b/nikola/data/themes/bootstrap4/templates/index_helper.tmpl @@ -0,0 +1,13 @@ +## -*- coding: utf-8 -*- +<%def name="html_pager()"> + %if prevlink or nextlink: + <ul class="pager postindexpager clearfix"> + %if prevlink: + <li class="previous"><a href="${prevlink}" rel="prev">${messages("Newer posts")}</a></li> + %endif + %if nextlink: + <li class="next"><a href="${nextlink}" rel="next">${messages("Older posts")}</a></li> + %endif + </ul> + %endif +</%def> diff --git a/nikola/data/themes/bootstrap3/templates/listing.tmpl b/nikola/data/themes/bootstrap4/templates/listing.tmpl index 44809d0..d9a4c56 100644 --- a/nikola/data/themes/bootstrap3/templates/listing.tmpl +++ b/nikola/data/themes/bootstrap4/templates/listing.tmpl @@ -1,15 +1,15 @@ ## -*- coding: utf-8 -*- <%inherit file="base.tmpl"/> -<%namespace name="ui" file="crumbs.tmpl" import="bar"/> +<%namespace name="ui" file="ui_helper.tmpl"/> <%block name="content"> -${ui.bar(crumbs)} +${ui.breadcrumbs(crumbs)} %if folders or files: <ul> % for name in folders: - <li><a href="${name|u}"><i class="glyphicon glyphicon-folder-open"></i> ${name|h}</a> + <li><a href="${name|h}">📂 ${name|h}</a> % endfor % for name in files: - <li><a href="${name|u}.html"><i class="glyphicon glyphicon-file"></i> ${name|h}</a> + <li><a href="${name|h}.html">📄 ${name|h}</a> % endfor </ul> %endif @@ -24,9 +24,7 @@ ${ui.bar(crumbs)} </%block> <%block name="sourcelink"> -% if source_link: - <li> - <a href="${source_link}" id="sourcelink">${messages("Source")}</a> - </li> +% if source_link and show_sourcelink: + ${ui.show_sourcelink(source_link)} % endif </%block> diff --git a/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl b/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl new file mode 100644 index 0000000..da0e920 --- /dev/null +++ b/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl @@ -0,0 +1,40 @@ +## -*- coding: utf-8 -*- +<%def name="page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5)"> +<nav aria-label="Page navigation"> + <ul class="pagination"> + % if prev_next_links_reversed: + % if nextlink: + <li class="page-item"><a href="${nextlink}" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">«</span></a></li> + % else: + <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">«</span></a></li> + % endif + % else: + % if prevlink: + <li class="page-item"><a href="${prevlink}" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">«</span></a></li> + % else: + <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">«</span></a></li> + % endif + % endif + % for i, link in enumerate(page_links): + % if abs(i - current_page) <= surrounding or i == 0 or i == len(page_links) - 1: + <li class="page-item ${'active' if i == current_page else ''}"><a href="${link}" class="page-link">${i + 1}${' <span class="sr-only">(current)</span>' if i == current_page else ''}</a></li> + % elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1: + <li class="page-item disabled"><a href="#" class="page-link" aria-label="…"><span aria-hidden="true">…</span></a></li> + % endif + % endfor + % if prev_next_links_reversed: + % if prevlink: + <li class="page-item"><a href="${prevlink}" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">»</span></a></li> + % else: + <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">»</span></a></li> + % endif + % else: + % if nextlink: + <li class="page-item"><a href="${nextlink}" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">»</span></a></li> + % else: + <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">»</span></a></li> + % endif + % endif + </ul> +</nav> +</%def> diff --git a/nikola/data/themes/bootstrap3/templates/post.tmpl b/nikola/data/themes/bootstrap4/templates/post.tmpl index 469c1e1..0d4248e 100644 --- a/nikola/data/themes/bootstrap3/templates/post.tmpl +++ b/nikola/data/themes/bootstrap4/templates/post.tmpl @@ -2,16 +2,15 @@ <%namespace name="helper" file="post_helper.tmpl"/> <%namespace name="pheader" file="post_header.tmpl"/> <%namespace name="comments" file="comments_helper.tmpl"/> +<%namespace name="math" file="math_helper.tmpl"/> +<%namespace name="ui" file="ui_helper.tmpl"/> <%inherit file="base.tmpl"/> <%block name="extra_head"> ${parent.extra_head()} % if post.meta('keywords'): - <meta name="keywords" content="${post.meta('keywords')|h}"> + <meta name="keywords" content="${smartjoin(', ', post.meta('keywords'))|h}"> % endif - %if post.description(): - <meta name="description" itemprop="description" content="${post.description()|h}"> - %endif <meta name="author" content="${post.author()|h}"> %if post.prev_post: <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html"> @@ -25,6 +24,7 @@ ${helper.open_graph_metadata(post)} ${helper.twitter_card_information(post)} ${helper.meta_translations(post)} + ${math.math_styles_ifpost(post)} </%block> <%block name="content"> @@ -45,15 +45,13 @@ ${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)} </section> % endif - ${helper.mathjax_script(post)} + ${math.math_scripts_ifpost(post)} </article> ${comments.comment_link_script()} </%block> <%block name="sourcelink"> % if show_sourcelink: - <li> - <a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a> - </li> + ${ui.show_sourcelink(post.source_link())} % endif </%block> diff --git a/nikola/data/themes/bootstrap3/templates/tags.tmpl b/nikola/data/themes/bootstrap4/templates/tags.tmpl index 061bb39..f1870f6 100644 --- a/nikola/data/themes/bootstrap3/templates/tags.tmpl +++ b/nikola/data/themes/bootstrap4/templates/tags.tmpl @@ -11,7 +11,7 @@ % for i in range(indent_change_before): <ul class="list-inline"> % endfor - <li><a class="reference badge" href="${link}">${text|h}</a> + <li class="list-inline-item"><a class="reference badge badge-secondary" href="${link}">${text|h}</a> % if indent_change_after <= 0: </li> % endif @@ -30,7 +30,7 @@ <ul class="list-inline"> % for text, link in items: % if text not in hidden_tags: - <li><a class="reference badge" href="${link}">${text|h}</a></li> + <li class="list-inline-item"><a class="reference badge badge-secondary" href="${link}">${text|h}</a></li> % endif % endfor </ul> diff --git a/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl b/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl new file mode 100644 index 0000000..7e884f9 --- /dev/null +++ b/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl @@ -0,0 +1,24 @@ +## -*- coding: utf-8 -*- +<%def name="breadcrumbs(crumbs)"> +%if crumbs: +<nav class="breadcrumbs"> +<ul class="breadcrumb"> + % for link, text in crumbs: + % if text != index_file: + % if link == '#': + <li class="breadcrumb-item active">${text.rsplit('.html', 1)[0]}</li> + % else: + <li class="breadcrumb-item"><a href="${link}">${text}</a></li> + % endif + % endif + % endfor +</ul> +</nav> +%endif +</%def> + +<%def name="show_sourcelink(sourcelink_href)"> + <li class="nav-item"> + <a href="${sourcelink_href}" id="sourcelink" class="nav-link">${messages("Source")}</a> + </li> +</%def> diff --git a/nikola/filters.py b/nikola/filters.py index b53e605..9d7e492 100644 --- a/nikola/filters.py +++ b/nikola/filters.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,25 +24,43 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Utility functions to help run filters on files.""" +"""Utility functions to help run filters on files. + +All filters defined in this module are registered in Nikola.__init__. +""" -from functools import wraps -import os import io import json +import os +import re import shutil +import shlex import subprocess import tempfile -import shlex +from functools import wraps import lxml +import requests + +from .utils import req_missing, LOGGER, slugify + try: import typogrify.filters as typo except ImportError: - typo = None # NOQA -import requests + typo = None + -from .utils import req_missing, LOGGER +class _ConfigurableFilter(object): + """Allow Nikola to configure filter with site's config.""" + + def __init__(self, **configuration_variables): + """Define which arguments to configure from which configuration variables.""" + self.configuration_variables = configuration_variables + + def __call__(self, f): + """Store configuration_variables as attribute of function.""" + f.configuration_variables = self.configuration_variables + return f def apply_to_binary_file(f): @@ -53,10 +71,10 @@ def apply_to_binary_file(f): in place. Reads files in binary mode. """ @wraps(f) - def f_in_file(fname): + def f_in_file(fname, *args, **kwargs): with open(fname, 'rb') as inf: data = inf.read() - data = f(data) + data = f(data, *args, **kwargs) with open(fname, 'wb+') as outf: outf.write(data) @@ -71,10 +89,10 @@ def apply_to_text_file(f): in place. Reads files in UTF-8. """ @wraps(f) - def f_in_file(fname): - with io.open(fname, 'r', encoding='utf-8') as inf: + def f_in_file(fname, *args, **kwargs): + with io.open(fname, 'r', encoding='utf-8-sig') as inf: data = inf.read() - data = f(data) + data = f(data, *args, **kwargs) with io.open(fname, 'w+', encoding='utf-8') as outf: outf.write(data) @@ -126,60 +144,76 @@ def runinplace(command, infile): shutil.rmtree(tmpdir) -def yui_compressor(infile): +@_ConfigurableFilter(executable='YUI_COMPRESSOR_EXECUTABLE') +def yui_compressor(infile, executable=None): """Run YUI Compressor on a file.""" - yuicompressor = False - try: - subprocess.call('yui-compressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) - yuicompressor = 'yui-compressor' - except Exception: - pass + yuicompressor = executable + if not yuicompressor: + try: + subprocess.call('yui-compressor', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + yuicompressor = 'yui-compressor' + except Exception: + pass if not yuicompressor: try: - subprocess.call('yuicompressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) + subprocess.call('yuicompressor', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) yuicompressor = 'yuicompressor' - except: + except Exception: raise Exception("yui-compressor is not installed.") return False return runinplace('{} --nomunge %1 -o %2'.format(yuicompressor), infile) -def closure_compiler(infile): +@_ConfigurableFilter(executable='CLOSURE_COMPILER_EXECUTABLE') +def closure_compiler(infile, executable='closure-compiler'): """Run closure-compiler on a file.""" - return runinplace('closure-compiler --warning_level QUIET --js %1 --js_output_file %2', infile) + return runinplace('{} --warning_level QUIET --js %1 --js_output_file %2'.format(executable), infile) -def optipng(infile): +@_ConfigurableFilter(executable='OPTIPNG_EXECUTABLE') +def optipng(infile, executable='optipng'): """Run optipng on a file.""" - return runinplace("optipng -preserve -o2 -quiet %1", infile) + return runinplace("{} -preserve -o2 -quiet %1".format(executable), infile) -def jpegoptim(infile): +@_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE') +def jpegoptim(infile, executable='jpegoptim'): """Run jpegoptim on a file.""" - return runinplace("jpegoptim -p --strip-all -q %1", infile) + return runinplace("{} -p --strip-all -q %1".format(executable), infile) +@_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE') +def jpegoptim_progressive(infile, executable='jpegoptim'): + """Run jpegoptim on a file and convert to progressive.""" + return runinplace("{} -p --strip-all --all-progressive -q %1".format(executable), infile) + + +@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') def html_tidy_withconfig(infile, executable='tidy5'): """Run HTML Tidy with tidy5.conf as config file.""" return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent -config tidy5.conf -modify %1", executable=executable) +@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') def html_tidy_nowrap(infile, executable='tidy5'): """Run HTML Tidy without line wrapping.""" return _html_tidy_runner(infile, "-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", executable=executable) +@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') def html_tidy_wrap(infile, executable='tidy5'): """Run HTML Tidy with line wrapping.""" return _html_tidy_runner(infile, "-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", executable=executable) +@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') def html_tidy_wrap_attr(infile, executable='tidy5'): """Run HTML tidy with line wrapping and attribute indentation.""" return _html_tidy_runner(infile, "-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", executable=executable) +@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') def html_tidy_mini(infile, executable='tidy5'): """Run HTML tidy with minimal settings.""" return _html_tidy_runner(infile, "-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", executable=executable) @@ -202,7 +236,7 @@ def html5lib_minify(data): import html5lib.serializer data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'), tree='lxml', - quote_attr_values=False, + quote_attr_values='spec', omit_optional_tags=True, minimize_boolean_attributes=True, strip_whitespace=True, @@ -218,7 +252,7 @@ def html5lib_xmllike(data): import html5lib.serializer data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'), tree='lxml', - quote_attr_values=True, + quote_attr_values='always', omit_optional_tags=False, strip_whitespace=False, alphabetical_attributes=True, @@ -232,21 +266,53 @@ def minify_lines(data): return data +def _run_typogrify(data, typogrify_filters, ignore_tags=None): + """Run typogrify with ignore support.""" + if ignore_tags is None: + ignore_tags = ["title"] + + data = _normalize_html(data) + + section_list = typo.process_ignores(data, ignore_tags) + + rendered_text = "" + for text_item, should_process in section_list: + if should_process: + for f in typogrify_filters: + text_item = f(text_item) + + rendered_text += text_item + + return rendered_text + + @apply_to_text_file def typogrify(data): """Prettify text with typogrify.""" if typo is None: req_missing(['typogrify'], 'use the typogrify filter', optional=True) return data + return _run_typogrify(data, [typo.amp, typo.widont, typo.smartypants, typo.caps, typo.initial_quotes]) - data = _normalize_html(data) - data = typo.amp(data) - data = typo.widont(data) - data = typo.smartypants(data) - # Disabled because of typogrify bug where it breaks <title> - # data = typo.caps(data) - data = typo.initial_quotes(data) - return data + +def _smarty_oldschool(text): + try: + import smartypants + except ImportError: + raise typo.TypogrifyError("Error in {% smartypants %} filter: The Python smartypants library isn't installed.") + else: + output = smartypants.convert_dashes_oldschool(text) + return output + + +@apply_to_text_file +def typogrify_oldschool(data): + """Prettify text with typogrify.""" + if typo is None: + req_missing(['typogrify'], 'use the typogrify_oldschool filter', optional=True) + return data + + return _run_typogrify(data, [typo.amp, typo.widont, _smarty_oldschool, typo.smartypants, typo.caps, typo.initial_quotes]) @apply_to_text_file @@ -256,28 +322,31 @@ def typogrify_sans_widont(data): # wrapping, see issue #1465 if typo is None: req_missing(['typogrify'], 'use the typogrify_sans_widont filter') + return data - data = _normalize_html(data) - data = typo.amp(data) - data = typo.smartypants(data) - # Disabled because of typogrify bug where it breaks <title> - # data = typo.caps(data) - data = typo.initial_quotes(data) - return data + return _run_typogrify(data, [typo.amp, typo.smartypants, typo.caps, typo.initial_quotes]) + + +@apply_to_text_file +def typogrify_custom(data, typogrify_filters, ignore_tags=None): + """Run typogrify with a custom list of fliter functions.""" + if typo is None: + req_missing(['typogrify'], 'use the typogrify filter', optional=True) + return data + return _run_typogrify(data, typogrify_filters, ignore_tags) @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) + template = re.search(r'<\!-- __NIKOLA_PHP_TEMPLATE_INJECTION source\:(.*) checksum\:(.*)__ -->', data) if template: source = template.group(1) - with io.open(source, "r", encoding="utf-8") as in_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: phpdata = in_file.read() _META_SEPARATOR = '(' + os.linesep * 2 + '|' + ('\n' * 2) + '|' + ("\r\n" * 2) + ')' phpdata = re.split(_META_SEPARATOR, phpdata, maxsplit=1)[-1] - phpdata = re.sub(template.group(0), phpdata, data) + phpdata = data.replace(template.group(0), phpdata) return phpdata else: return data @@ -285,9 +354,9 @@ def php_template_injection(data): @apply_to_text_file def cssminify(data): - """Minify CSS using http://cssminifier.com/.""" + """Minify CSS using https://cssminifier.com/.""" try: - url = 'http://cssminifier.com/raw' + url = 'https://cssminifier.com/raw' _data = {'input': data} response = requests.post(url, data=_data) if response.status_code != 200: @@ -301,9 +370,9 @@ def cssminify(data): @apply_to_text_file def jsminify(data): - """Minify JS using http://javascript-minifier.com/.""" + """Minify JS using https://javascript-minifier.com/.""" try: - url = 'http://javascript-minifier.com/raw' + url = 'https://javascript-minifier.com/raw' _data = {'input': data} response = requests.post(url, data=_data) if response.status_code != 200: @@ -334,9 +403,108 @@ def _normalize_html(data): """Pass HTML through LXML to clean it up, if possible.""" try: data = lxml.html.tostring(lxml.html.fromstring(data), encoding='unicode') - except: + except Exception: pass return '<!DOCTYPE html>\n' + data +# The function is used in other filters, so the decorator cannot be used directly. normalize_html = apply_to_text_file(_normalize_html) + + +@_ConfigurableFilter(xpath_list='HEADER_PERMALINKS_XPATH_LIST', file_blacklist='HEADER_PERMALINKS_FILE_BLACKLIST') +def add_header_permalinks(fname, xpath_list=None, file_blacklist=None): + """Post-process HTML via lxml to add header permalinks Sphinx-style.""" + # Blacklist requires custom file handling + file_blacklist = file_blacklist or [] + if fname in file_blacklist: + return + with io.open(fname, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + doc = lxml.html.document_fromstring(data) + # Get language for slugify + try: + lang = doc.attrib['lang'] # <html lang="…"> + except KeyError: + # Circular import workaround (utils imports filters) + from nikola.utils import LocaleBorg + lang = LocaleBorg().current_lang + + xpath_set = set() + if not xpath_list: + xpath_list = ['*//div[@class="e-content entry-content"]//{hx}'] + for xpath_expr in xpath_list: + for hx in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + xpath_set.add(xpath_expr.format(hx=hx)) + for x in xpath_set: + nodes = doc.findall(x) + for node in nodes: + parent = node.getparent() + if 'id' in node.attrib: + hid = node.attrib['id'] + elif 'id' in parent.attrib: + # docutils: <div> has an ID and contains the header + hid = parent.attrib['id'] + else: + # Using force-mode, because not every character can appear in a + # HTML id + node.attrib['id'] = slugify(node.text_content(), lang, True) + hid = node.attrib['id'] + + new_node = lxml.html.fragment_fromstring('<a href="#{0}" class="headerlink" title="Permalink to this heading">¶</a>'.format(hid)) + node.append(new_node) + + with io.open(fname, 'w', encoding='utf-8') as outf: + outf.write('<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding="unicode")) + + +@_ConfigurableFilter(top_classes='DEDUPLICATE_IDS_TOP_CLASSES') +@apply_to_text_file +def deduplicate_ids(data, top_classes=None): + """Post-process HTML via lxml to deduplicate IDs.""" + if not top_classes: + top_classes = ('postpage', 'storypage') + doc = lxml.html.document_fromstring(data) + elements = doc.xpath('//*') + all_ids = [element.attrib.get('id') for element in elements] + seen_ids = set() + duplicated_ids = set() + for i in all_ids: + if i is not None and i in seen_ids: + duplicated_ids.add(i) + else: + seen_ids.add(i) + + if duplicated_ids: + # Well, that sucks. + for i in duplicated_ids: + # Results are ordered the same way they are ordered in document + offending_elements = doc.xpath('//*[@id="{}"]'.format(i)) + counter = 2 + # If this is a story or a post, do it from top to bottom, because + # updates to those are more likely to appear at the bottom of pages. + # For anything else, including indexes, do it from bottom to top, + # because new posts appear at the top of pages. + # We also leave the first result out, so there is one element with + # "plain" ID + if any(doc.find_class(c) for c in top_classes): + off = offending_elements[1:] + else: + off = offending_elements[-2::-1] + for e in off: + new_id = i + while new_id in seen_ids: + new_id = '{0}-{1}'.format(i, counter) + counter += 1 + e.attrib['id'] = new_id + seen_ids.add(new_id) + # Find headerlinks that we can fix. + headerlinks = e.find_class('headerlink') + for hl in headerlinks: + # We might get headerlinks of child elements + if hl.attrib['href'] == '#' + i: + hl.attrib['href'] = '#' + new_id + break + return '<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='unicode') + else: + return data diff --git a/nikola/hierarchy_utils.py b/nikola/hierarchy_utils.py new file mode 100644 index 0000000..8993518 --- /dev/null +++ b/nikola/hierarchy_utils.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Hierarchy utility functions.""" + +import natsort + +__all__ = ('TreeNode', 'clone_treenode', 'flatten_tree_structure', + 'sort_classifications', 'join_hierarchical_category_path', + 'parse_escaped_hierarchical_category_name',) + + +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 + + # The indent levels and changes allow to render a tree structure + # without keeping track of all that information during rendering. + # + # The indent_change_before is the different between the current + # comment's level and the previous comment's level; if the number + # is positive, the current level is indented further in, and if it + # is negative, it is indented further out. Positive values can be + # used to open HTML tags for each opened level. + # + # The indent_change_after is the difference between the next + # comment's level and the current comment's level. Negative values + # can be used to close HTML tags for each closed level. + # + # The indent_levels list contains one entry (index, count) per + # level, informing about the index of the current comment on that + # level and the count of comments on that level (before a comment + # of a higher level comes). This information can be used to render + # tree indicators, for example to generate a tree such as: + # + # +--- [(0,3)] + # +-+- [(1,3)] + # | +--- [(1,3), (0,2)] + # | +-+- [(1,3), (1,2)] + # | +--- [(1,3), (1,2), (0, 1)] + # +-+- [(2,3)] + # +- [(2,3), (0,1)] + # + # (The lists used as labels represent the content of the + # 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: + path.append(curr) + curr = curr.parent + return reversed(path) + + def get_children(self): + """Get children of a node.""" + return self.children + + def __str__(self): + """Stringify node (return name).""" + return self.name + + def _repr_partial(self): + """Return partial representation.""" + if self.parent: + return "{0}/{1!r}".format(self.parent._repr_partial(), self.name) + else: + return repr(self.name) + + def __repr__(self): + """Return programmer-friendly node representation.""" + return "<TreeNode {0}>".format(self._repr_partial()) + + +def clone_treenode(treenode, parent=None, acceptor=lambda x: True): + """Clone a TreeNode. + + Children are only cloned if `acceptor` returns `True` when + applied on them. + + Returns the cloned node if it has children or if `acceptor` + applied to it returns `True`. In case neither applies, `None` + is returned. + """ + # Copy standard TreeNode stuff + node_clone = TreeNode(treenode.name, parent) + node_clone.children = [clone_treenode(node, parent=node_clone, acceptor=acceptor) for node in treenode.children] + node_clone.children = [node for node in node_clone.children if node] + node_clone.indent_levels = treenode.indent_levels + node_clone.indent_change_before = treenode.indent_change_before + node_clone.indent_change_after = treenode.indent_change_after + if hasattr(treenode, 'classification_path'): + # Copy stuff added by taxonomies_classifier plugin + node_clone.classification_path = treenode.classification_path + node_clone.classification_name = treenode.classification_name + + # Accept this node if there are no children (left) and acceptor fails + if not node_clone.children and not acceptor(treenode): + return None + return node_clone + + +def flatten_tree_structure(root_list): + """Flatten a tree.""" + elements = [] + + def generate(input_list, indent_levels_so_far): + """Generate flat list of nodes.""" + for index, element in enumerate(input_list): + # add to destination + elements.append(element) + # compute and set indent levels + indent_levels = indent_levels_so_far + [(index, len(input_list))] + element.indent_levels = indent_levels + # add children + children = element.get_children() + element.children_count = len(children) + generate(children, indent_levels) + + generate(root_list, []) + # Add indent change counters + level = 0 + last_element = None + for element in elements: + new_level = len(element.indent_levels) + # Compute level change before this element + change = new_level - level + if last_element is not None: + last_element.indent_change_after = change + element.indent_change_before = change + # Update variables + level = new_level + last_element = element + # Set level change after last element + if last_element is not None: + last_element.indent_change_after = -level + return elements + + +def parse_escaped_hierarchical_category_name(category_name): + """Parse a category name.""" + result = [] + current = None + index = 0 + next_backslash = category_name.find('\\', index) + next_slash = category_name.find('/', index) + while index < len(category_name): + if next_backslash == -1 and next_slash == -1: + current = (current if current else "") + category_name[index:] + index = len(category_name) + elif next_slash >= 0 and (next_backslash == -1 or next_backslash > next_slash): + result.append((current if current else "") + category_name[index:next_slash]) + current = '' + index = next_slash + 1 + next_slash = category_name.find('/', index) + else: + if len(category_name) == next_backslash + 1: + raise Exception("Unexpected '\\' in '{0}' at last position!".format(category_name)) + esc_ch = category_name[next_backslash + 1] + if esc_ch not in {'/', '\\'}: + raise Exception("Unknown escape sequence '\\{0}' in '{1}'!".format(esc_ch, category_name)) + current = (current if current else "") + category_name[index:next_backslash] + esc_ch + index = next_backslash + 2 + next_backslash = category_name.find('\\', index) + if esc_ch == '/': + next_slash = category_name.find('/', index) + if current is not None: + result.append(current) + return result + + +def join_hierarchical_category_path(category_path): + """Join a category path.""" + def escape(s): + """Espace one part of category path.""" + return s.replace('\\', '\\\\').replace('/', '\\/') + + return '/'.join([escape(p) for p in category_path]) + + +def sort_classifications(taxonomy, classifications, lang): + """Sort the given list of classifications of the given taxonomy and language. + + ``taxonomy`` must be a ``Taxonomy`` plugin. + ``classifications`` must be an iterable collection of + classification strings for that taxonomy. + ``lang`` is the language the classifications are for. + + The result will be returned as a sorted list. Sorting will + happen according to the way the complete classification + hierarchy for the taxonomy is sorted. + """ + if taxonomy.has_hierarchy: + # To sort a hierarchy of classifications correctly, we first + # build a tree out of them (and mark for each node whether it + # appears in the list), then sort the tree node-wise, and finally + # collapse the tree into a list of recombined classifications. + + # Step 1: build hierarchy. Here, each node consists of a boolean + # flag (node appears in list) and a dictionary mapping path elements + # to nodes. + root = [False, {}] + for classification in classifications: + node = root + for elt in taxonomy.extract_hierarchy(classification): + if elt not in node[1]: + node[1][elt] = [False, {}] + node = node[1][elt] + node[0] = True + # Step 2: sort hierarchy. The result for a node is a pair + # (flag, subnodes), where subnodes is a list of pairs (name, subnode). + + def sort_node(node, level=0): + """Return sorted node, with children as `(name, node)` list instead of a dictionary.""" + keys = natsort.natsorted(node[1].keys(), alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(keys, lang, level) + subnodes = [] + for key in keys: + subnodes.append((key, sort_node(node[1][key], level + 1))) + return (node[0], subnodes) + + root = sort_node(root) + # Step 3: collapse the tree structure into a linear sorted list, + # with a node coming before its children. + + def append_node(classifications, node, path=()): + """Append the node and then its children to the classifications list.""" + if node[0]: + classifications.append(taxonomy.recombine_classification_from_hierarchy(path)) + for key, subnode in node[1]: + append_node(classifications, subnode, path + (key, )) + + classifications = [] + append_node(classifications, root) + return classifications + else: + # Sorting a flat hierarchy is simpler. We pre-sort with + # natsorted and call taxonomy.sort_classifications. + classifications = natsort.natsorted(classifications, alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications, lang) + return classifications diff --git a/nikola/image_processing.py b/nikola/image_processing.py index e0096b2..04d4e64 100644 --- a/nikola/image_processing.py +++ b/nikola/image_processing.py @@ -26,35 +26,24 @@ """Process images.""" -from __future__ import unicode_literals import datetime +import gzip import os -import lxml import re -import gzip +import lxml import piexif +from PIL import ExifTags, Image from nikola import utils -Image = None -try: - from PIL import ExifTags, Image # NOQA -except ImportError: - try: - import ExifTags - import Image as _Image - Image = _Image - except ImportError: - pass - EXIF_TAG_NAMES = {} class ImageProcessor(object): """Apply image operations.""" - image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff'] + image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff', '.webp'] def _fill_exif_tag_names(self): """Connect EXIF tag names to numeric values.""" @@ -92,101 +81,136 @@ class ImageProcessor(object): return exif or None - def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}): - """Make a copy of the image in the requested size.""" - if not Image or os.path.splitext(src)[1] in ['.svg', '.svgz']: - self.resize_svg(src, dst, max_size, bigger_panoramas) + def resize_image(self, src, dst=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None): + """Make a copy of the image in the requested size(s). + + max_sizes should be a list of sizes, and the image would be resized to fit in a + square of each size (preserving aspect ratio). + + dst_paths is a list of the destination paths, and should be the same length as max_sizes. + + Backwards compatibility: + + * If max_sizes is None, it's set to [max_size] + * If dst_paths is None, it's set to [dst] + * Either max_size or max_sizes should be set + * Either dst or dst_paths should be set + """ + if dst_paths is None: + dst_paths = [dst] + if max_sizes is None: + max_sizes = [max_size] + if len(max_sizes) != len(dst_paths): + raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes)) + extension = os.path.splitext(src)[1].lower() + if extension in {'.svg', '.svgz'}: + self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas) return - im = Image.open(src) - size = w, h = im.size - if w > max_size or h > max_size: - size = max_size, max_size - - # Panoramas get larger thumbnails because they look *awful* - if bigger_panoramas and w > 2 * h: - size = min(w, max_size * 4), min(w, max_size * 4) - - try: - exif = piexif.load(im.info["exif"]) - except KeyError: - exif = None - # Inside this if, we can manipulate exif as much as - # we want/need and it will be preserved if required - if exif is not None: + + _im = Image.open(src) + + # The jpg exclusion is Issue #3332 + is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'} + + exif = None + if "exif" in _im.info: + exif = piexif.load(_im.info["exif"]) # Rotate according to EXIF - value = exif['0th'].get(piexif.ImageIFD.Orientation, 1) - if value in (3, 4): - im = im.transpose(Image.ROTATE_180) - elif value in (5, 6): - im = im.transpose(Image.ROTATE_270) - elif value in (7, 8): - im = im.transpose(Image.ROTATE_90) - if value in (2, 4, 5, 7): - im = im.transpose(Image.FLIP_LEFT_RIGHT) - exif['0th'][piexif.ImageIFD.Orientation] = 1 - - try: - im.thumbnail(size, Image.ANTIALIAS) - if exif is not None and preserve_exif_data: - # Put right size in EXIF data - w, h = im.size - if '0th' in exif: - exif["0th"][piexif.ImageIFD.ImageWidth] = w - exif["0th"][piexif.ImageIFD.ImageLength] = h - if 'Exif' in exif: - exif["Exif"][piexif.ExifIFD.PixelXDimension] = w - exif["Exif"][piexif.ExifIFD.PixelYDimension] = h - # Filter EXIF data as required - exif = self.filter_exif(exif, exif_whitelist) - im.save(dst, exif=piexif.dump(exif)) - else: - im.save(dst) - except Exception as e: - self.logger.warn("Can't process {0}, using original " - "image! ({1})".format(src, e)) - utils.copy_file(src, dst) - - def resize_svg(self, src, dst, max_size, bigger_panoramas): - """Make a copy of an svg at the requested size.""" - try: - # Resize svg based on viewport hacking. - # note that this can also lead to enlarged svgs - if src.endswith('.svgz'): - with gzip.GzipFile(src, 'rb') as op: - xml = op.read() - else: - with open(src, 'rb') as op: - xml = op.read() - tree = lxml.etree.XML(xml) - width = tree.attrib['width'] - height = tree.attrib['height'] - w = int(re.search("[0-9]+", width).group(0)) - h = int(re.search("[0-9]+", height).group(0)) - # calculate new size preserving aspect ratio. - ratio = float(w) / h - # Panoramas get larger thumbnails because they look *awful* - if bigger_panoramas and w > 2 * h: - max_size = max_size * 4 - if w > h: - w = max_size - h = max_size / ratio - else: - w = max_size * ratio - h = max_size - w = int(w) - h = int(h) - tree.attrib.pop("width") - tree.attrib.pop("height") - tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h) - if dst.endswith('.svgz'): - op = gzip.GzipFile(dst, 'wb') - else: - op = open(dst, 'wb') - op.write(lxml.etree.tostring(tree)) - op.close() - except (KeyError, AttributeError) as e: - self.logger.warn("No width/height in %s. Original exception: %s" % (src, e)) - utils.copy_file(src, dst) + if "0th" in exif: + value = exif['0th'].get(piexif.ImageIFD.Orientation, 1) + if value in (3, 4): + _im = _im.transpose(Image.ROTATE_180) + elif value in (5, 6): + _im = _im.transpose(Image.ROTATE_270) + elif value in (7, 8): + _im = _im.transpose(Image.ROTATE_90) + if value in (2, 4, 5, 7): + _im = _im.transpose(Image.FLIP_LEFT_RIGHT) + exif['0th'][piexif.ImageIFD.Orientation] = 1 + exif = self.filter_exif(exif, exif_whitelist) + + icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None + + for dst, max_size in zip(dst_paths, max_sizes): + if is_animated: # Animated gif, leave as-is + utils.copy_file(src, dst) + continue + + im = _im.copy() + + size = w, h = im.size + if w > max_size or h > max_size: + size = max_size, max_size + # Panoramas get larger thumbnails because they look *awful* + if bigger_panoramas and w > 2 * h: + size = min(w, max_size * 4), min(w, max_size * 4) + try: + im.thumbnail(size, Image.ANTIALIAS) + save_args = {} + if icc_profile: + save_args['icc_profile'] = icc_profile + + if exif is not None and preserve_exif_data: + # Put right size in EXIF data + w, h = im.size + if '0th' in exif: + exif["0th"][piexif.ImageIFD.ImageWidth] = w + exif["0th"][piexif.ImageIFD.ImageLength] = h + if 'Exif' in exif: + exif["Exif"][piexif.ExifIFD.PixelXDimension] = w + exif["Exif"][piexif.ExifIFD.PixelYDimension] = h + # Filter EXIF data as required + save_args['exif'] = piexif.dump(exif) + + im.save(dst, **save_args) + except Exception as e: + self.logger.warning("Can't process {0}, using original " + "image! ({1})".format(src, e)) + utils.copy_file(src, dst) + + def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas): + """Make a copy of an svg at the requested sizes.""" + # Resize svg based on viewport hacking. + # note that this can also lead to enlarged svgs + if src.endswith('.svgz'): + with gzip.GzipFile(src, 'rb') as op: + xml = op.read() + else: + with open(src, 'rb') as op: + xml = op.read() + + for dst, max_size in zip(dst_paths, max_sizes): + try: + tree = lxml.etree.XML(xml) + width = tree.attrib['width'] + height = tree.attrib['height'] + w = int(re.search("[0-9]+", width).group(0)) + h = int(re.search("[0-9]+", height).group(0)) + # calculate new size preserving aspect ratio. + ratio = float(w) / h + # Panoramas get larger thumbnails because they look *awful* + if bigger_panoramas and w > 2 * h: + max_size = max_size * 4 + if w > h: + w = max_size + h = max_size / ratio + else: + w = max_size * ratio + h = max_size + w = int(w) + h = int(h) + tree.attrib.pop("width") + tree.attrib.pop("height") + tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h) + if dst.endswith('.svgz'): + op = gzip.GzipFile(dst, 'wb') + else: + op = open(dst, 'wb') + op.write(lxml.etree.tostring(tree)) + op.close() + except (KeyError, AttributeError) as e: + self.logger.warning("No width/height in %s. Original exception: %s" % (src, e)) + utils.copy_file(src, dst) def image_date(self, src): """Try to figure out the date of the image.""" @@ -194,6 +218,7 @@ class ImageProcessor(object): try: im = Image.open(src) exif = im._getexif() + im.close() except Exception: exif = None if exif is not None: diff --git a/nikola/log.py b/nikola/log.py new file mode 100644 index 0000000..9960ba1 --- /dev/null +++ b/nikola/log.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Logging support.""" + +import enum +import logging +import warnings + +from nikola import DEBUG + +__all__ = ( + "get_logger", + "LOGGER", +) + + +# Handlers/formatters +class ApplicationWarning(Exception): + """An application warning, raised in strict mode.""" + + pass + + +class StrictModeExceptionHandler(logging.StreamHandler): + """A logging handler that raises an exception on warnings.""" + + def emit(self, record: logging.LogRecord) -> None: + """Emit a logging record.""" + if record.levelno >= logging.WARNING: + raise ApplicationWarning(self.format(record)) + + +class ColorfulFormatter(logging.Formatter): + """Stream handler with colors.""" + + _colorful = False + + def format(self, record: logging.LogRecord) -> str: + """Format a message and add colors to it.""" + message = super().format(record) + return self.wrap_in_color(record).format(message) + + def wrap_in_color(self, record: logging.LogRecord) -> str: + """Return the colorized string for this record.""" + if not self._colorful: + return "{}" + if record.levelno >= logging.ERROR: + return "\033[1;31m{}\033[0m" + elif record.levelno >= logging.WARNING: + return "\033[1;33m{}\033[0m" + elif record.levelno >= logging.INFO: + return "\033[1m{}\033[0m" + return "\033[37m{}\033[0m" + + +# Initial configuration +class LoggingMode(enum.Enum): + """Logging mode options.""" + + NORMAL = 0 + STRICT = 1 + QUIET = 2 + + +def configure_logging(logging_mode: LoggingMode = LoggingMode.NORMAL) -> None: + """Configure logging for Nikola. + + This method can be called multiple times, previous configuration will be overridden. + """ + if DEBUG: + logging.root.level = logging.DEBUG + else: + logging.root.level = logging.INFO + + if logging_mode == LoggingMode.QUIET: + logging.root.handlers = [] + return + + handler = logging.StreamHandler() + handler.setFormatter( + ColorfulFormatter( + fmt="[%(asctime)s] %(levelname)s: %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + + handlers = [handler] + if logging_mode == LoggingMode.STRICT: + handlers.append(StrictModeExceptionHandler()) + + logging.root.handlers = handlers + + +configure_logging() + + +# For compatibility with old code written with Logbook in mind +# TODO remove in v9 +def patch_notice_level(logger: logging.Logger) -> logging.Logger: + """Patch logger to issue WARNINGs with logger.notice.""" + logger.notice = logger.warning + return logger + + +# User-facing loggers +def get_logger(name: str, handlers=None) -> logging.Logger: + """Get a logger with handlers attached.""" + logger = logging.getLogger(name) + if handlers is not None: + for h in handlers: + logger.addHandler(h) + return patch_notice_level(logger) + + +LOGGER = get_logger("Nikola") + + +# Push warnings to logging +def showwarning(message, category, filename, lineno, file=None, line=None): + """Show a warning (from the warnings module) to the user.""" + try: + n = category.__name__ + except AttributeError: + n = str(category) + get_logger(n).warning("{0}:{1}: {2}".format(filename, lineno, message)) + + +warnings.showwarning = showwarning diff --git a/nikola/metadata_extractors.py b/nikola/metadata_extractors.py new file mode 100644 index 0000000..2377dc2 --- /dev/null +++ b/nikola/metadata_extractors.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Default metadata extractors and helper functions.""" + +import re +from enum import Enum +from io import StringIO + +import natsort + +from nikola.plugin_categories import MetadataExtractor +from nikola.utils import unslugify + +__all__ = ('MetaCondition', 'MetaPriority', 'MetaSource', 'check_conditions') +_default_extractors = [] +DEFAULT_EXTRACTOR_NAME = 'nikola' +DEFAULT_EXTRACTOR = None + + +class MetaCondition(Enum): + """Conditions for extracting metadata.""" + + config_bool = 1 + config_present = 2 + extension = 3 + compiler = 4 + first_line = 5 + never = -1 + + +class MetaPriority(Enum): + """Priority of metadata. + + An extractor is used if and only if the higher-priority extractors returned nothing. + """ + + override = 1 + specialized = 2 + normal = 3 + fallback = 4 + + +class MetaSource(Enum): + """Source of metadata.""" + + text = 1 + filename = 2 + + +def check_conditions(post, filename: str, conditions: list, config: dict, source_text: str) -> bool: + """Check the conditions for a metadata extractor.""" + for ct, arg in conditions: + if any(( + ct == MetaCondition.config_bool and not config.get(arg, False), + ct == MetaCondition.config_present and arg not in config, + ct == MetaCondition.extension and not filename.endswith(arg), + ct == MetaCondition.compiler and (post is None or post.compiler.name != arg), + ct == MetaCondition.never + )): + return False + elif ct == MetaCondition.first_line: + if not source_text or not source_text.startswith(arg + '\n'): + return False + return True + + +def classify_extractor(extractor: MetadataExtractor, metadata_extractors_by: dict): + """Classify an extractor and add it to the metadata_extractors_by dict.""" + global DEFAULT_EXTRACTOR + if extractor.name == DEFAULT_EXTRACTOR_NAME: + DEFAULT_EXTRACTOR = extractor + metadata_extractors_by['priority'][extractor.priority].append(extractor) + metadata_extractors_by['source'][extractor.source].append(extractor) + metadata_extractors_by['name'][extractor.name] = extractor + metadata_extractors_by['all'].append(extractor) + + +def load_defaults(site, metadata_extractors_by: dict): + """Load default metadata extractors.""" + for extractor in _default_extractors: + extractor.site = site + classify_extractor(extractor, metadata_extractors_by) + + +def is_extractor(extractor) -> bool: # pragma: no cover + """Check if a given class is an extractor.""" + return isinstance(extractor, MetadataExtractor) + + +def default_metadata_extractors_by() -> dict: + """Return the default metadata_extractors_by dictionary.""" + d = { + 'priority': {}, + 'source': {}, + 'name': {}, + 'all': [] + } + + for i in MetaPriority: + d['priority'][i] = [] + for i in MetaSource: + d['source'][i] = [] + + return d + + +def _register_default(extractor: type) -> type: + """Register a default extractor.""" + _default_extractors.append(extractor()) + return extractor + + +@_register_default +class NikolaMetadata(MetadataExtractor): + """Extractor for Nikola-style metadata.""" + + name = 'nikola' + source = MetaSource.text + priority = MetaPriority.normal + supports_write = True + split_metadata_re = re.compile('\n\n') + nikola_re = re.compile(r'^\s*\.\. (.*?): (.*)') + map_from = 'nikola' # advertised in values mapping only + + def _extract_metadata_from_text(self, source_text: str) -> dict: + """Extract metadata from text.""" + outdict = {} + for line in source_text.split('\n'): + match = self.nikola_re.match(line) + if match: + k, v = match.group(1), match.group(2) + if v: + outdict[k] = v + return outdict + + def write_metadata(self, metadata: dict, comment_wrap=False) -> str: + """Write metadata in this extractor’s format.""" + metadata = metadata.copy() + order = ('title', 'slug', 'date', 'tags', 'category', 'link', 'description', 'type') + f = '.. {0}: {1}' + meta = [] + for k in order: + try: + meta.append(f.format(k, metadata.pop(k))) + except KeyError: + pass + # Leftover metadata (user-specified/non-default). + for k in natsort.natsorted(list(metadata.keys()), alg=natsort.ns.F | natsort.ns.IC): + meta.append(f.format(k, metadata[k])) + data = '\n'.join(meta) + if comment_wrap is True: + comment_wrap = ('<!--', '-->') + if comment_wrap: + return '\n'.join((comment_wrap[0], data, comment_wrap[1], '', '')) + else: + return data + '\n\n' + + +@_register_default +class YAMLMetadata(MetadataExtractor): + """Extractor for YAML metadata.""" + + name = 'yaml' + source = MetaSource.text + conditions = ((MetaCondition.first_line, '---'),) + requirements = [('ruamel.yaml', 'ruamel.yaml', 'YAML')] + supports_write = True + split_metadata_re = re.compile('\n---\n') + map_from = 'yaml' + priority = MetaPriority.specialized + + def _extract_metadata_from_text(self, source_text: str) -> dict: + """Extract metadata from text.""" + from ruamel.yaml import YAML + yaml = YAML(typ='safe') + meta = yaml.load(source_text[4:]) + # We expect empty metadata to be '', not None + for k in meta: + if meta[k] is None: + meta[k] = '' + return meta + + def write_metadata(self, metadata: dict, comment_wrap=False) -> str: + """Write metadata in this extractor’s format.""" + from ruamel.yaml import YAML + yaml = YAML(typ='safe') + yaml.default_flow_style = False + stream = StringIO() + yaml.dump(metadata, stream) + stream.seek(0) + return '\n'.join(('---', stream.read().strip(), '---', '')) + + +@_register_default +class TOMLMetadata(MetadataExtractor): + """Extractor for TOML metadata.""" + + name = 'toml' + source = MetaSource.text + conditions = ((MetaCondition.first_line, '+++'),) + requirements = [('toml', 'toml', 'TOML')] + supports_write = True + split_metadata_re = re.compile('\n\\+\\+\\+\n') + map_from = 'toml' + priority = MetaPriority.specialized + + def _extract_metadata_from_text(self, source_text: str) -> dict: + """Extract metadata from text.""" + import toml + return toml.loads(source_text[4:]) + + def write_metadata(self, metadata: dict, comment_wrap=False) -> str: + """Write metadata in this extractor’s format.""" + import toml + return '\n'.join(('+++', toml.dumps(metadata).strip(), '+++', '')) + + +@_register_default +class FilenameRegexMetadata(MetadataExtractor): + """Extractor for filename metadata.""" + + name = 'filename_regex' + source = MetaSource.filename + priority = MetaPriority.fallback + conditions = [(MetaCondition.config_bool, 'FILE_METADATA_REGEXP')] + + def _extract_metadata_from_text(self, source_text: str) -> dict: + """Extract metadata from text.""" + # This extractor does not use the source text, and as such, this method returns an empty dict. + return {} + + def extract_filename(self, filename: str, lang: str) -> dict: + """Try to read 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 + """ + match = re.match(self.site.config['FILE_METADATA_REGEXP'], filename) + meta = {} + + if match: + for key, value in match.groupdict().items(): + k = key.lower().strip() # metadata must be lowercase + if k == 'title' and self.site.config['FILE_METADATA_UNSLUGIFY_TITLES']: + meta[k] = unslugify(value, lang, discard_numbers=False) + else: + meta[k] = value + + return meta diff --git a/nikola/nikola.py b/nikola/nikola.py index 0a62360..86d81e6 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,39 +26,32 @@ """The main Nikola site object.""" -from __future__ import print_function, unicode_literals -import io -from collections import defaultdict -from copy import copy -from pkg_resources import resource_filename import datetime -import locale -import os +import io import json +import functools +import logging +import operator +import os import sys -import natsort import mimetypes -try: - from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote -except ImportError: - from urllib.parse import urlparse, urlsplit, urlunsplit, urljoin, unquote # NOQA - -try: - import pyphen -except ImportError: - pyphen = None +from collections import defaultdict +from copy import copy +from urllib.parse import urlparse, urlsplit, urlunsplit, urljoin, unquote, parse_qs import dateutil.tz -import logging -import PyRSS2Gen as rss import lxml.etree import lxml.html -from yapsy.PluginManager import PluginManager +import natsort +import PyRSS2Gen as rss +from pkg_resources import resource_filename from blinker import signal +from yapsy.PluginManager import PluginManager +from . import DEBUG, SHOW_TRACEBACKS, filters, utils, hierarchy_utils, shortcodes +from . import metadata_extractors +from .metadata_extractors import default_metadata_extractors_by from .post import Post # NOQA -from .state import Persistor -from . import DEBUG, utils, shortcodes from .plugin_categories import ( Command, LateTask, @@ -66,6 +59,7 @@ from .plugin_categories import ( CompilerExtension, MarkdownExtension, RestExtension, + MetadataExtractor, ShortcodePlugin, Task, TaskMultiplier, @@ -73,7 +67,14 @@ from .plugin_categories import ( SignalHandler, ConfigPlugin, PostScanner, + Taxonomy, ) +from .state import Persistor + +try: + import pyphen +except ImportError: + pyphen = None if DEBUG: logging.basicConfig(level=logging.DEBUG) @@ -84,26 +85,25 @@ else: DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>' DEFAULT_FEED_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>' -# Default pattern for translation files' names -DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}' - config_changed = utils.config_changed __all__ = ('Nikola',) -# We store legal values for some setting here. For internal use. +# We store legal values for some settings here. For internal use. LEGAL_VALUES = { + 'DEFAULT_THEME': 'bootblog4', 'COMMENT_SYSTEM': [ 'disqus', 'facebook', - 'googleplus', 'intensedebate', 'isso', - 'livefyre', 'muut', + 'commento', + 'utterances', ], 'TRANSLATIONS': { + 'af': 'Afrikaans', 'ar': 'Arabic', 'az': 'Azerbaijani', 'bg': 'Bulgarian', @@ -121,16 +121,20 @@ LEGAL_VALUES = { 'fa': 'Persian', 'fi': 'Finnish', 'fr': 'French', + 'fur': 'Friulian', 'gl': 'Galician', 'he': 'Hebrew', 'hi': 'Hindi', 'hr': 'Croatian', 'hu': 'Hungarian', + 'ia': 'Interlingua', 'id': 'Indonesian', 'it': 'Italian', ('ja', '!jp'): 'Japanese', 'ko': 'Korean', 'lt': 'Lithuanian', + 'ml': 'Malayalam', + 'mr': 'Marathi', 'nb': 'Norwegian (Bokmål)', 'nl': 'Dutch', 'pa': 'Punjabi', @@ -145,61 +149,14 @@ LEGAL_VALUES = { 'sr_latin': 'Serbian (Latin)', 'sv': 'Swedish', 'te': 'Telugu', + 'th': 'Thai', ('tr', '!tr_TR'): 'Turkish', - 'ur': 'Urdu', 'uk': 'Ukrainian', + 'ur': 'Urdu', + 'vi': 'Vietnamese', 'zh_cn': 'Chinese (Simplified)', 'zh_tw': 'Chinese (Traditional)' }, - '_WINDOWS_LOCALE_GUESSES': { - # TODO incomplete - # some languages may need that the appropriate Microsoft Language Pack be installed. - "ar": "Arabic", - "az": "Azeri (Latin)", - "bg": "Bulgarian", - "bs": "Bosnian", - "ca": "Catalan", - "cs": "Czech", - "da": "Danish", - "de": "German", - "el": "Greek", - "en": "English", - # "eo": "Esperanto", # Not available - "es": "Spanish", - "et": "Estonian", - "eu": "Basque", - "fa": "Persian", # Persian - "fi": "Finnish", - "fr": "French", - "gl": "Galician", - "he": "Hebrew", - "hi": "Hindi", - "hr": "Croatian", - "hu": "Hungarian", - "id": "Indonesian", - "it": "Italian", - "ja": "Japanese", - "ko": "Korean", - "nb": "Norwegian", # Not Bokmål, as Windows doesn't find it for unknown reasons. - "nl": "Dutch", - "pa": "Punjabi", - "pl": "Polish", - "pt": "Portuguese_Portugal", - "pt_br": "Portuguese_Brazil", - "ru": "Russian", - "sk": "Slovak", - "sl": "Slovenian", - "sq": "Albanian", - "sr": "Serbian", - "sr_latin": "Serbian (Latin)", - "sv": "Swedish", - "te": "Telugu", - "tr": "Turkish", - "uk": "Ukrainian", - "ur": "Urdu", - "zh_cn": "Chinese_China", # Chinese (Simplified) - "zh_tw": "Chinese_Taiwan", # Chinese (Traditional) - }, '_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 @@ -208,95 +165,126 @@ LEGAL_VALUES = { # This dict is currently empty. }, + 'LOCALES_BASE': { + # A list of locale mappings to apply for every site. Can be overridden in the config. + 'sr_latin': 'sr_Latn', + }, 'RTL_LANGUAGES': ('ar', 'fa', 'he', 'ur'), - 'COLORBOX_LOCALES': defaultdict( - str, - ar='ar', - bg='bg', - ca='ca', - cs='cs', - cz='cs', - da='da', - de='de', - en='', - es='es', - et='et', - fa='fa', - fi='fi', - fr='fr', - he='he', - hr='hr', - hu='hu', - id='id', - it='it', - ja='ja', - ko='kr', # kr is South Korea, ko is the Korean language - lt='lt', - nb='no', - nl='nl', - pl='pl', - pt='pt-BR', # hope nobody will mind - pt_br='pt-BR', - ru='ru', - sk='sk', - sl='si', # country code is si, language code is sl, colorbox is wrong - sr='sr', # warning: this is serbian in Latin alphabet - sr_latin='sr', - sv='sv', - tr='tr', - uk='uk', - zh_cn='zh-CN', - zh_tw='zh-TW' - ), - 'MOMENTJS_LOCALES': defaultdict( - str, - ar='ar', - az='az', - bg='bg', - bn='bn', - bs='bs', - ca='ca', - cs='cs', - cz='cs', - da='da', - de='de', - el='el', - en='en', - eo='eo', - es='es', - et='et', - eu='eu', - fa='fa', - fi='fi', - fr='fr', - gl='gl', - hi='hi', - he='he', - hr='hr', - hu='hu', - id='id', - it='it', - ja='ja', - ko='ko', - lt='lt', - nb='nb', - nl='nl', - pl='pl', - pt='pt', - pt_br='pt-br', - ru='ru', - sk='sk', - sl='sl', - sq='sq', - sr='sr-cyrl', - sr_latin='sr', - sv='sv', - tr='tr', - uk='uk', - zh_cn='zh-cn', - zh_tw='zh-tw' - ), + 'LUXON_LOCALES': defaultdict(lambda: 'en', **{ + 'af': 'af', + 'ar': 'ar', + 'az': 'az', + 'bg': 'bg', + 'bn': 'bn', + 'bs': 'bs', + 'ca': 'ca', + 'cs': 'cs', + 'cz': 'cs', + 'da': 'da', + 'de': 'de', + 'el': 'el', + 'en': 'en', + 'eo': 'eo', + 'es': 'es', + 'et': 'et', + 'eu': 'eu', + 'fa': 'fa', + 'fi': 'fi', + 'fr': 'fr', + 'fur': 'fur', + 'gl': 'gl', + 'hi': 'hi', + 'he': 'he', + 'hr': 'hr', + 'hu': 'hu', + 'ia': 'ia', + 'id': 'id', + 'it': 'it', + 'ja': 'ja', + 'ko': 'ko', + 'lt': 'lt', + 'ml': 'ml', + 'mr': 'mr', + 'nb': 'nb', + 'nl': 'nl', + 'pa': 'pa', + 'pl': 'pl', + 'pt': 'pt', + 'pt_br': 'pt-BR', + 'ru': 'ru', + 'sk': 'sk', + 'sl': 'sl', + 'sq': 'sq', + 'sr': 'sr-Cyrl', + 'sr_latin': 'sr-Latn', + 'sv': 'sv', + 'te': 'te', + 'tr': 'tr', + 'th': 'th', + 'uk': 'uk', + 'ur': 'ur', + 'vi': 'vi', + 'zh_cn': 'zh-CN', + 'zh_tw': 'zh-TW' + }), + # TODO: remove in v9 + 'MOMENTJS_LOCALES': defaultdict(lambda: 'en', **{ + 'af': 'af', + 'ar': 'ar', + 'az': 'az', + 'bg': 'bg', + 'bn': 'bn', + 'bs': 'bs', + 'ca': 'ca', + 'cs': 'cs', + 'cz': 'cs', + 'da': 'da', + 'de': 'de', + 'el': 'el', + 'en': 'en', + 'eo': 'eo', + 'es': 'es', + 'et': 'et', + 'eu': 'eu', + 'fa': 'fa', + 'fi': 'fi', + 'fr': 'fr', + 'gl': 'gl', + 'hi': 'hi', + 'he': 'he', + 'hr': 'hr', + 'hu': 'hu', + 'id': 'id', + 'it': 'it', + 'ja': 'ja', + 'ko': 'ko', + 'lt': 'lt', + 'ml': 'ml', + 'mr': 'mr', + 'nb': 'nb', + 'nl': 'nl', + 'pa': 'pa-in', + 'pl': 'pl', + 'pt': 'pt', + 'pt_br': 'pt-br', + 'ru': 'ru', + 'sk': 'sk', + 'sl': 'sl', + 'sq': 'sq', + 'sr': 'sr-cyrl', + 'sr_latin': 'sr', + 'sv': 'sv', + 'te': 'te', + 'tr': 'tr', + 'th': 'th', + 'uk': 'uk', + 'ur': 'ur', + 'vi': 'vi', + 'zh_cn': 'zh-cn', + 'zh_tw': 'zh-tw' + }), 'PYPHEN_LOCALES': { + 'af': 'af', 'bg': 'bg', 'ca': 'ca', 'cs': 'cs', @@ -326,12 +314,14 @@ LEGAL_VALUES = { 'uk': 'uk', }, 'DOCUTILS_LOCALES': { + 'af': 'af', 'ca': 'ca', 'da': 'da', 'de': 'de', 'en': 'en', 'eo': 'eo', 'es': 'es', + 'fa': 'fa', 'fi': 'fi', 'fr': 'fr', 'gl': 'gl', @@ -339,6 +329,7 @@ LEGAL_VALUES = { 'it': 'it', 'ja': 'ja', 'lt': 'lt', + 'nl': 'nl', 'pl': 'pl', 'pt': 'pt_br', # hope nobody will mind 'pt_br': 'pt_br', @@ -347,9 +338,21 @@ LEGAL_VALUES = { 'sv': 'sv', 'zh_cn': 'zh_cn', 'zh_tw': 'zh_tw' - } + }, + "METADATA_MAPPING": ["yaml", "toml", "rest_docinfo", "markdown_metadata"], +} + +# Mapping old pre-taxonomy plugin names to new post-taxonomy plugin names +TAXONOMY_COMPATIBILITY_PLUGIN_NAME_MAP = { + "render_archive": ["classify_archive"], + "render_authors": ["classify_authors"], + "render_indexes": ["classify_page_index", "classify_sections"], # "classify_indexes" removed from list (see #2591 and special-case logic below) + "render_tags": ["classify_categories", "classify_tags"], } +# Default value for the pattern used to name translated files +DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}' + def _enclosure(post, lang): """Add an enclosure to RSS.""" @@ -360,7 +363,7 @@ def _enclosure(post, lang): except KeyError: length = 0 except ValueError: - utils.LOGGER.warn("Invalid enclosure length for post {0}".format(post.source_path)) + utils.LOGGER.warning("Invalid enclosure length for post {0}".format(post.source_path)) length = 0 url = enclosure mime = mimetypes.guess_type(url)[0] @@ -374,7 +377,7 @@ class Nikola(object): """ def __init__(self, **config): - """Setup proper environment for running tasks.""" + """Initialize proper environment for running tasks.""" # Register our own path handlers self.path_handlers = { 'slug': self.slug_path, @@ -398,8 +401,9 @@ class Nikola(object): self._template_system = None self._THEMES = None self._MESSAGES = None + self.filters = {} self.debug = DEBUG - self.loghandlers = utils.STDERR_HANDLER # TODO remove on v8 + self.show_tracebacks = SHOW_TRACEBACKS self.colorful = config.pop('__colorful__', False) self.invariant = config.pop('__invariant__', False) self.quiet = config.pop('__quiet__', False) @@ -409,6 +413,7 @@ class Nikola(object): self.configured = bool(config) self.injected_deps = defaultdict(list) self.shortcode_registry = {} + self.metadata_extractors_by = default_metadata_extractors_by() self.rst_transforms = [] self.template_hooks = { @@ -425,7 +430,6 @@ class Nikola(object): # This is the default config self.config = { - 'ANNOTATIONS': False, 'ARCHIVE_PATH': "", 'ARCHIVE_FILENAME': "archive.html", 'ARCHIVES_ARE_INDEXES': False, @@ -435,16 +439,25 @@ class Nikola(object): 'AUTHORLIST_MINIMUM_POSTS': 1, 'BLOG_AUTHOR': 'Default Author', 'BLOG_TITLE': 'Default Title', + 'BLOG_EMAIL': '', 'BLOG_DESCRIPTION': 'Default Description', 'BODY_END': "", 'CACHE_FOLDER': 'cache', + 'CATEGORIES_INDEX_PATH': '', 'CATEGORY_PATH': None, # None means: same as TAG_PATH 'CATEGORY_PAGES_ARE_INDEXES': None, # None means: same as TAG_PAGES_ARE_INDEXES - 'CATEGORY_PAGES_DESCRIPTIONS': {}, - 'CATEGORY_PAGES_TITLES': {}, + 'CATEGORY_DESCRIPTIONS': {}, + 'CATEGORY_TITLES': {}, 'CATEGORY_PREFIX': 'cat_', 'CATEGORY_ALLOW_HIERARCHIES': False, 'CATEGORY_OUTPUT_FLAT_HIERARCHY': False, + 'CATEGORY_DESTPATH_AS_DEFAULT': False, + 'CATEGORY_DESTPATH_TRIM_PREFIX': False, + 'CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY': True, + 'CATEGORY_DESTPATH_NAMES': {}, + 'CATEGORY_PAGES_FOLLOW_DESTPATH': False, + 'CATEGORY_TRANSLATIONS': [], + 'CATEGORY_TRANSLATIONS_ADD_DEFAULTS': False, 'CODE_COLOR_SCHEME': 'default', 'COMMENT_SYSTEM': 'disqus', 'COMMENTS_IN_GALLERIES': False, @@ -461,13 +474,21 @@ class Nikola(object): }, 'CONTENT_FOOTER': '', 'CONTENT_FOOTER_FORMATS': {}, + 'RSS_COPYRIGHT': '', + 'RSS_COPYRIGHT_PLAIN': '', + 'RSS_COPYRIGHT_FORMATS': {}, 'COPY_SOURCES': True, + 'CREATE_ARCHIVE_NAVIGATION': False, 'CREATE_MONTHLY_ARCHIVE': False, 'CREATE_SINGLE_ARCHIVE': False, 'CREATE_FULL_ARCHIVES': False, 'CREATE_DAILY_ARCHIVE': False, - 'DATE_FORMAT': '%Y-%m-%d %H:%M', - 'JS_DATE_FORMAT': 'YYYY-MM-DD HH:mm', + 'DATE_FORMAT': 'yyyy-MM-dd HH:mm', + 'DISABLE_INDEXES': False, + 'DISABLE_MAIN_ATOM_FEED': False, + 'DISABLE_MAIN_RSS_FEED': False, + 'MOMENTJS_DATE_FORMAT': 'YYYY-MM-DD HH:mm', + 'LUXON_DATE_FORMAT': {}, 'DATE_FANCINESS': 0, 'DEFAULT_LANG': "en", 'DEPLOY_COMMANDS': {'default': []}, @@ -481,6 +502,7 @@ class Nikola(object): 'FAVICONS': (), 'FEED_LENGTH': 10, 'FILE_METADATA_REGEXP': None, + 'FILE_METADATA_UNSLUGIFY_TITLES': True, 'ADDITIONAL_METADATA': {}, 'FILES_FOLDERS': {'files': ''}, 'FILTERS': {}, @@ -488,12 +510,15 @@ class Nikola(object): 'FRONT_INDEX_HEADER': '', 'GALLERY_FOLDERS': {'galleries': 'galleries'}, 'GALLERY_SORT_BY_DATE': True, + 'GALLERIES_USE_THUMBNAIL': False, + 'GALLERIES_DEFAULT_THUMBNAIL': None, 'GLOBAL_CONTEXT_FILLER': [], 'GZIP_COMMAND': None, 'GZIP_FILES': False, 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'), 'HIDDEN_AUTHORS': [], 'HIDDEN_TAGS': [], + 'HIDE_REST_DOCINFO': False, 'HIDDEN_CATEGORIES': [], 'HYPHENATE': False, 'IMAGE_FOLDERS': {'images': ''}, @@ -501,6 +526,7 @@ class Nikola(object): 'INDEX_FILE': 'index.html', 'INDEX_TEASERS': False, 'IMAGE_THUMBNAIL_SIZE': 400, + 'IMAGE_THUMBNAIL_FORMAT': '{name}.thumbnail{ext}', 'INDEXES_TITLE': "", 'INDEXES_PAGES': "", 'INDEXES_PAGES_MAIN': False, @@ -509,48 +535,50 @@ class Nikola(object): 'INDEX_PATH': '', 'IPYNB_CONFIG': {}, 'KATEX_AUTO_RENDER': '', - 'LESS_COMPILER': 'lessc', - 'LESS_OPTIONS': [], 'LICENSE': '', 'LINK_CHECK_WHITELIST': [], 'LISTINGS_FOLDERS': {'listings': 'listings'}, 'LOGO_URL': '', + 'DEFAULT_PREVIEW_IMAGE': None, 'NAVIGATION_LINKS': {}, - 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'], # FIXME: Add 'extras' in v8 + 'NAVIGATION_ALT_LINKS': {}, + 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite', 'extra'], + 'MARKDOWN_EXTENSION_CONFIGS': {}, 'MAX_IMAGE_SIZE': 1280, 'MATHJAX_CONFIG': '', + 'METADATA_FORMAT': 'nikola', + 'METADATA_MAPPING': {}, + 'MULTIPLE_AUTHORS_PER_POST': False, + 'NEW_POST_DATE_PATH': False, + 'NEW_POST_DATE_PATH_FORMAT': '%Y/%m/%d', 'OLD_THEME_SUPPORT': True, 'OUTPUT_FOLDER': 'output', 'POSTS': (("posts/*.txt", "posts", "post.tmpl"),), - 'POSTS_SECTIONS': True, - 'POSTS_SECTION_COLORS': {}, - 'POSTS_SECTION_ARE_INDEXES': True, - 'POSTS_SECTION_DESCRIPTIONS': "", - 'POSTS_SECTION_FROM_META': False, - 'POSTS_SECTION_NAME': "", - 'POSTS_SECTION_TITLE': "{name}", 'PRESERVE_EXIF_DATA': False, - # TODO: change in v8 - 'PAGES': (("stories/*.txt", "stories", "story.tmpl"),), + 'PRESERVE_ICC_PROFILES': False, + 'PAGES': (("pages/*.txt", "pages", "page.tmpl"),), 'PANDOC_OPTIONS': [], - 'PRETTY_URLS': False, + 'PRETTY_URLS': True, 'FUTURE_IS_NOW': False, 'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK, 'REDIRECTIONS': [], 'ROBOTS_EXCLUSIONS': [], 'GENERATE_ATOM': False, + 'ATOM_EXTENSION': '.atom', + 'ATOM_PATH': '', + 'ATOM_FILENAME_BASE': 'index', 'FEED_TEASERS': True, 'FEED_PLAIN': False, - 'FEED_PREVIEWIMAGE': True, 'FEED_READ_MORE_LINK': DEFAULT_FEED_READ_MORE_LINK, 'FEED_LINKS_APPEND_QUERY': False, 'GENERATE_RSS': True, + 'RSS_EXTENSION': '.xml', 'RSS_LINK': None, 'RSS_PATH': '', - 'SASS_COMPILER': 'sass', - 'SASS_OPTIONS': [], + 'RSS_FILENAME_BASE': 'rss', 'SEARCH_FORM': '', 'SHOW_BLOG_TITLE': True, + 'SHOW_INDEX_PAGE_NAVIGATION': False, 'SHOW_SOURCELINK': True, 'SHOW_UNTRANSLATED_POSTS': True, 'SLUG_AUTHOR_PATH': True, @@ -558,47 +586,53 @@ class Nikola(object): 'SOCIAL_BUTTONS_CODE': '', 'SITE_URL': 'https://example.com/', 'PAGE_INDEX': False, - 'STRIP_INDEXES': False, - 'SITEMAP_INCLUDE_FILELESS_DIRS': True, + 'SECTION_PATH': '', + 'STRIP_INDEXES': True, 'TAG_PATH': 'categories', 'TAG_PAGES_ARE_INDEXES': False, - 'TAG_PAGES_DESCRIPTIONS': {}, - 'TAG_PAGES_TITLES': {}, + 'TAG_DESCRIPTIONS': {}, + 'TAG_TITLES': {}, + 'TAG_TRANSLATIONS': [], + 'TAG_TRANSLATIONS_ADD_DEFAULTS': False, 'TAGS_INDEX_PATH': '', 'TAGLIST_MINIMUM_POSTS': 1, 'TEMPLATE_FILTERS': {}, - 'THEME': 'bootstrap3', + 'THEME': LEGAL_VALUES['DEFAULT_THEME'], 'THEME_COLOR': '#5670d4', # light "corporate blue" - 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky', - 'THEME_REVEAL_CONFIG_TRANSITION': 'cube', + 'THEME_CONFIG': {}, 'THUMBNAIL_SIZE': 180, - 'UNSLUGIFY_TITLES': False, # WARNING: conf.py.in overrides this with True for backwards compatibility + 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, 'URL_TYPE': 'rel_path', - 'USE_BASE_TAG': False, 'USE_BUNDLES': True, 'USE_CDN': False, 'USE_CDN_WARNING': True, + 'USE_REST_DOCINFO_METADATA': False, 'USE_FILENAME_AS_TITLE': True, 'USE_KATEX': False, - 'USE_OPEN_GRAPH': True, 'USE_SLUGIFY': True, + 'USE_TAG_METADATA': True, 'TIMEZONE': 'UTC', - 'WRITE_TAG_CLOUD': True, + 'WARN_ABOUT_TAG_METADATA': True, 'DEPLOY_DRAFTS': True, 'DEPLOY_FUTURE': False, 'SCHEDULE_ALL': False, 'SCHEDULE_RULE': '', - 'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}}, 'DEMOTE_HEADERS': 1, 'GITHUB_SOURCE_BRANCH': 'master', 'GITHUB_DEPLOY_BRANCH': 'gh-pages', 'GITHUB_REMOTE_NAME': 'origin', 'GITHUB_COMMIT_SOURCE': False, # WARNING: conf.py.in overrides this with True for backwards compatibility + 'META_GENERATOR_TAG': True, + 'REST_FILE_INSERTION_ENABLED': True, + 'TYPES_TO_HIDE_TITLE': [], } # set global_context for template rendering self._GLOBAL_CONTEXT = {} + # dependencies for all pages, not included in global context + self.ALL_PAGE_DEPS = {} + self.config.update(config) # __builtins__ contains useless cruft @@ -612,13 +646,22 @@ class Nikola(object): self.config['__invariant__'] = self.invariant self.config['__quiet__'] = self.quiet - # Make sure we have sane NAVIGATION_LINKS. + # Use ATOM_PATH when set + self.config['ATOM_PATH'] = self.config['ATOM_PATH'] or self.config['INDEX_PATH'] + + # Make sure we have sane NAVIGATION_LINKS and NAVIGATION_ALT_LINKS. if not self.config['NAVIGATION_LINKS']: self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()} + if not self.config['NAVIGATION_ALT_LINKS']: + self.config['NAVIGATION_ALT_LINKS'] = {self.config['DEFAULT_LANG']: ()} # Translatability configuration. self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS', {self.config['DEFAULT_LANG']: ''}) + for k, v in self.config['TRANSLATIONS'].items(): + if os.path.isabs(v): + self.config['TRANSLATIONS'][k] = os.path.relpath(v, '/') + utils.TranslatableSetting.default_lang = self.config['DEFAULT_LANG'] self.TRANSLATABLE_SETTINGS = ('BLOG_AUTHOR', @@ -631,27 +674,40 @@ class Nikola(object): 'BODY_END', 'EXTRA_HEAD_DATA', 'NAVIGATION_LINKS', + 'NAVIGATION_ALT_LINKS', 'FRONT_INDEX_HEADER', 'INDEX_READ_MORE_LINK', 'FEED_READ_MORE_LINK', 'INDEXES_TITLE', - 'POSTS_SECTION_COLORS', - 'POSTS_SECTION_DESCRIPTIONS', - 'POSTS_SECTION_NAME', - 'POSTS_SECTION_TITLE', + 'CATEGORY_DESTPATH_NAMES', 'INDEXES_PAGES', 'INDEXES_PRETTY_PAGE_URL', + 'THEME_CONFIG', # PATH options (Issue #1914) + 'ARCHIVE_PATH', + 'ARCHIVE_FILENAME', 'TAG_PATH', 'TAGS_INDEX_PATH', 'CATEGORY_PATH', + 'CATEGORIES_INDEX_PATH', + 'SECTION_PATH', + 'INDEX_PATH', + 'ATOM_PATH', + 'RSS_PATH', + 'RSS_FILENAME_BASE', + 'ATOM_FILENAME_BASE', + 'AUTHOR_PATH', 'DATE_FORMAT', - 'JS_DATE_FORMAT', + 'LUXON_DATE_FORMAT', + 'MOMENTJS_DATE_FORMAT', # TODO: remove in v9 + 'RSS_COPYRIGHT', + 'RSS_COPYRIGHT_PLAIN', + # Issue #2970 + 'MARKDOWN_EXTENSION_CONFIGS', ) self._GLOBAL_CONTEXT_TRANSLATABLE = ('blog_author', 'blog_title', - 'blog_desc', # TODO: remove in v8 'blog_description', 'license', 'content_footer', @@ -661,22 +717,40 @@ class Nikola(object): 'extra_head_data', 'date_format', 'js_date_format', - 'posts_section_colors', - 'posts_section_descriptions', - 'posts_section_name', - 'posts_section_title', + 'luxon_date_format', 'front_index_header', + 'theme_config', ) - # WARNING: navigation_links SHOULD NOT be added to the list above. + + self._ALL_PAGE_DEPS_TRANSLATABLE = ('atom_path', + 'rss_path', + 'rss_filename_base', + 'atom_filename_base', + ) + # WARNING: navigation_(alt_)links SHOULD NOT be added to the list above. # Themes ask for [lang] there and we should provide it. - # We first have to massage JS_DATE_FORMAT, otherwise we run into trouble + # Luxon setup is a dict of dicts, so we need to set up the default here. + if not self.config['LUXON_DATE_FORMAT']: + self.config['LUXON_DATE_FORMAT'] = {self.config['DEFAULT_LANG']: {'preset': False, 'format': 'yyyy-MM-dd HH:mm'}} + # TODO: remove Moment.js stuff in v9 if 'JS_DATE_FORMAT' in self.config: - if isinstance(self.config['JS_DATE_FORMAT'], dict): - for k in self.config['JS_DATE_FORMAT']: - self.config['JS_DATE_FORMAT'][k] = json.dumps(self.config['JS_DATE_FORMAT'][k]) + utils.LOGGER.warning("Moment.js was replaced by Luxon in the default themes, which uses different date formats.") + utils.LOGGER.warning("If you’re using a built-in theme, set LUXON_DATE_FORMAT. If your theme uses Moment.js, you can silence this warning by renaming JS_DATE_FORMAT to MOMENTJS_DATE_FORMAT.") + utils.LOGGER.warning("Sample Luxon config: LUXON_DATE_FORMAT = " + str(self.config['LUXON_DATE_FORMAT'])) + self.config['MOMENTJS_DATE_FORMAT'] = self.config['LUXON_DATE_FORMAT'] + + # We first have to massage MOMENTJS_DATE_FORMAT and LUXON_DATE_FORMAT, otherwise we run into trouble + if 'MOMENTJS_DATE_FORMAT' in self.config: + if isinstance(self.config['MOMENTJS_DATE_FORMAT'], dict): + for k in self.config['MOMENTJS_DATE_FORMAT']: + self.config['MOMENTJS_DATE_FORMAT'][k] = json.dumps(self.config['MOMENTJS_DATE_FORMAT'][k]) else: - self.config['JS_DATE_FORMAT'] = json.dumps(self.config['JS_DATE_FORMAT']) + self.config['MOMENTJS_DATE_FORMAT'] = json.dumps(self.config['MOMENTJS_DATE_FORMAT']) + + if 'LUXON_DATE_FORMAT' in self.config: + for k in self.config['LUXON_DATE_FORMAT']: + self.config['LUXON_DATE_FORMAT'][k] = json.dumps(self.config['LUXON_DATE_FORMAT'][k]) for i in self.TRANSLATABLE_SETTINGS: try: @@ -686,25 +760,104 @@ class Nikola(object): # A EXIF_WHITELIST implies you want to keep EXIF data if self.config['EXIF_WHITELIST'] and not self.config['PRESERVE_EXIF_DATA']: - utils.LOGGER.warn('Setting EXIF_WHITELIST implies PRESERVE_EXIF_DATA is set to True') + utils.LOGGER.warning('Setting EXIF_WHITELIST implies PRESERVE_EXIF_DATA is set to True') self.config['PRESERVE_EXIF_DATA'] = True # Setting PRESERVE_EXIF_DATA with an empty EXIF_WHITELIST implies 'keep everything' if self.config['PRESERVE_EXIF_DATA'] and not self.config['EXIF_WHITELIST']: - utils.LOGGER.warn('You are setting PRESERVE_EXIF_DATA and not EXIF_WHITELIST so EXIF data is not really kept.') + utils.LOGGER.warning('You are setting PRESERVE_EXIF_DATA and not EXIF_WHITELIST so EXIF data is not really kept.') + + if 'UNSLUGIFY_TITLES' in self.config: + utils.LOGGER.warning('The UNSLUGIFY_TITLES setting was renamed to FILE_METADATA_UNSLUGIFY_TITLES.') + self.config['FILE_METADATA_UNSLUGIFY_TITLES'] = self.config['UNSLUGIFY_TITLES'] + + if 'TAG_PAGES_TITLES' in self.config: + utils.LOGGER.warning('The TAG_PAGES_TITLES setting was renamed to TAG_TITLES.') + self.config['TAG_TITLES'] = self.config['TAG_PAGES_TITLES'] + + if 'TAG_PAGES_DESCRIPTIONS' in self.config: + utils.LOGGER.warning('The TAG_PAGES_DESCRIPTIONS setting was renamed to TAG_DESCRIPTIONS.') + self.config['TAG_DESCRIPTIONS'] = self.config['TAG_PAGES_DESCRIPTIONS'] + + if 'CATEGORY_PAGES_TITLES' in self.config: + utils.LOGGER.warning('The CATEGORY_PAGES_TITLES setting was renamed to CATEGORY_TITLES.') + self.config['CATEGORY_TITLES'] = self.config['CATEGORY_PAGES_TITLES'] + + if 'CATEGORY_PAGES_DESCRIPTIONS' in self.config: + utils.LOGGER.warning('The CATEGORY_PAGES_DESCRIPTIONS setting was renamed to CATEGORY_DESCRIPTIONS.') + self.config['CATEGORY_DESCRIPTIONS'] = self.config['CATEGORY_PAGES_DESCRIPTIONS'] + + if 'DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED' in self.config: + utils.LOGGER.warning('The DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED setting was renamed and split to DISABLE_INDEXES and DISABLE_MAIN_ATOM_FEED.') + self.config['DISABLE_INDEXES'] = self.config['DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED'] + self.config['DISABLE_MAIN_ATOM_FEED'] = self.config['DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED'] + + if 'DISABLE_INDEXES_PLUGIN_RSS_FEED' in self.config: + utils.LOGGER.warning('The DISABLE_INDEXES_PLUGIN_RSS_FEED setting was renamed to DISABLE_MAIN_RSS_FEED.') + self.config['DISABLE_MAIN_RSS_FEED'] = self.config['DISABLE_INDEXES_PLUGIN_RSS_FEED'] + + for val in self.config['DATE_FORMAT'].values.values(): + if '%' in val: + utils.LOGGER.error('The DATE_FORMAT setting needs to be upgraded.') + utils.LOGGER.warning("Nikola now uses CLDR-style date strings. http://cldr.unicode.org/translation/date-time") + utils.LOGGER.warning("Example: %Y-%m-%d %H:%M ==> yyyy-MM-dd HH:mm") + utils.LOGGER.warning("(note it’s different to what moment.js uses!)") + sys.exit(1) + + # Silently upgrade LOCALES (remove encoding) + locales = LEGAL_VALUES['LOCALES_BASE'] + if 'LOCALES' in self.config: + for k, v in self.config['LOCALES'].items(): + self.config['LOCALES'][k] = v.split('.')[0] + locales.update(self.config['LOCALES']) + self.config['LOCALES'] = locales + + if self.config.get('POSTS_SECTIONS'): + utils.LOGGER.warning("The sections feature has been removed and its functionality has been merged into categories.") + utils.LOGGER.warning("For more information on how to migrate, please read: https://getnikola.com/blog/upgrading-to-nikola-v8.html#sections-were-replaced-by-categories") + + for section_config_suffix, cat_config_suffix in ( + ('DESCRIPTIONS', 'DESCRIPTIONS'), + ('TITLE', 'TITLES'), + ('TRANSLATIONS', 'TRANSLATIONS') + ): + section_config = 'POSTS_SECTION_' + section_config_suffix + cat_config = 'CATEGORY_' + cat_config_suffix + if section_config in self.config: + self.config[section_config].update(self.config[cat_config]) + self.config[cat_config] = self.config[section_config] + + self.config['CATEGORY_DESTPATH_NAMES'] = self.config.get('POSTS_SECTION_NAME', {}) + # Need to mark this translatable manually. + self.config['CATEGORY_DESTPATH_NAMES'] = utils.TranslatableSetting('CATEGORY_DESTPATH_NAMES', self.config['CATEGORY_DESTPATH_NAMES'], self.config['TRANSLATIONS']) + + self.config['CATEGORY_DESTPATH_AS_DEFAULT'] = not self.config.get('POSTS_SECTION_FROM_META') + utils.LOGGER.info("Setting CATEGORY_DESTPATH_AS_DEFAULT = " + str(self.config['CATEGORY_DESTPATH_AS_DEFAULT'])) + + if self.config.get('CATEGORY_PAGES_FOLLOW_DESTPATH') and (not self.config.get('CATEGORY_ALLOW_HIERARCHIES') or self.config.get('CATEGORY_OUTPUT_FLAT_HIERARCHY')): + utils.LOGGER.error('CATEGORY_PAGES_FOLLOW_DESTPATH requires CATEGORY_ALLOW_HIERARCHIES = True, CATEGORY_OUTPUT_FLAT_HIERARCHY = False.') + sys.exit(1) + + # The Utterances comment system has a required configuration value + if self.config.get('COMMENT_SYSTEM') == 'utterances': + utterances_config = self.config.get('GLOBAL_CONTEXT', {}).get('utterances_config', {}) + if not ('issue-term' in utterances_config or 'issue-number' in utterances_config): + utils.LOGGER.error("COMMENT_SYSTEM = 'utterances' must have either GLOBAL_CONTEXT['utterances_config']['issue-term'] or GLOBAL_CONTEXT['utterances_config']['issue-term'] defined.") - # Handle CONTENT_FOOTER properly. - # We provide the arguments to format in CONTENT_FOOTER_FORMATS. + # Handle CONTENT_FOOTER and RSS_COPYRIGHT* properly. + # We provide the arguments to format in CONTENT_FOOTER_FORMATS and RSS_COPYRIGHT_FORMATS. self.config['CONTENT_FOOTER'].langformat(self.config['CONTENT_FOOTER_FORMATS']) + self.config['RSS_COPYRIGHT'].langformat(self.config['RSS_COPYRIGHT_FORMATS']) + self.config['RSS_COPYRIGHT_PLAIN'].langformat(self.config['RSS_COPYRIGHT_FORMATS']) # propagate USE_SLUGIFY utils.USE_SLUGIFY = self.config['USE_SLUGIFY'] # Make sure we have pyphen installed if we are using it if self.config.get('HYPHENATE') and pyphen is None: - utils.LOGGER.warn('To use the hyphenation, you have to install ' - 'the "pyphen" package.') - utils.LOGGER.warn('Setting HYPHENATE to False.') + utils.LOGGER.warning('To use the hyphenation, you have to install ' + 'the "pyphen" package.') + utils.LOGGER.warning('Setting HYPHENATE to False.') self.config['HYPHENATE'] = False # FIXME: Internally, we still use post_pages because it's a pain to change it @@ -714,114 +867,40 @@ class Nikola(object): for i1, i2, i3 in self.config['PAGES']: self.config['post_pages'].append([i1, i2, i3, False]) - # RSS_TEASERS has been replaced with FEED_TEASERS - # TODO: remove on v8 - if 'RSS_TEASERS' in config: - utils.LOGGER.warn('The RSS_TEASERS option is deprecated, use FEED_TEASERS instead.') - if 'FEED_TEASERS' in config: - utils.LOGGER.warn('FEED_TEASERS conflicts with RSS_TEASERS, ignoring RSS_TEASERS.') - self.config['FEED_TEASERS'] = config['RSS_TEASERS'] - - # RSS_PLAIN has been replaced with FEED_PLAIN - # TODO: remove on v8 - if 'RSS_PLAIN' in config: - utils.LOGGER.warn('The RSS_PLAIN option is deprecated, use FEED_PLAIN instead.') - if 'FEED_PLAIN' in config: - utils.LOGGER.warn('FEED_PLIN conflicts with RSS_PLAIN, ignoring RSS_PLAIN.') - self.config['FEED_PLAIN'] = config['RSS_PLAIN'] - - # RSS_LINKS_APPEND_QUERY has been replaced with FEED_LINKS_APPEND_QUERY - # TODO: remove on v8 - if 'RSS_LINKS_APPEND_QUERY' in config: - utils.LOGGER.warn('The RSS_LINKS_APPEND_QUERY option is deprecated, use FEED_LINKS_APPEND_QUERY instead.') - if 'FEED_LINKS_APPEND_QUERY' in config: - utils.LOGGER.warn('FEED_LINKS_APPEND_QUERY conflicts with RSS_LINKS_APPEND_QUERY, ignoring RSS_LINKS_APPEND_QUERY.') - self.config['FEED_LINKS_APPEND_QUERY'] = config['RSS_LINKS_APPEND_QUERY'] - - # RSS_READ_MORE_LINK has been replaced with FEED_READ_MORE_LINK - # TODO: remove on v8 - if 'RSS_READ_MORE_LINK' in config: - utils.LOGGER.warn('The RSS_READ_MORE_LINK option is deprecated, use FEED_READ_MORE_LINK instead.') - if 'FEED_READ_MORE_LINK' in config: - utils.LOGGER.warn('FEED_READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring RSS_READ_MORE_LINK') - self.config['FEED_READ_MORE_LINK'] = utils.TranslatableSetting('FEED_READ_MORE_LINK', config['RSS_READ_MORE_LINK'], self.config['TRANSLATIONS']) - - # DEFAULT_TRANSLATIONS_PATTERN was changed from "p.e.l" to "p.l.e" - # TODO: remove on v8 - if 'TRANSLATIONS_PATTERN' not in self.config: - if len(self.config.get('TRANSLATIONS', {})) > 1: - utils.LOGGER.warn('You do not have a TRANSLATIONS_PATTERN set in your config, yet you have multiple languages.') - utils.LOGGER.warn('Setting TRANSLATIONS_PATTERN to the pre-v6 default ("{path}.{ext}.{lang}").') - utils.LOGGER.warn('Please add the proper pattern to your conf.py. (The new default in v7 is "{0}".)'.format(DEFAULT_TRANSLATIONS_PATTERN)) - self.config['TRANSLATIONS_PATTERN'] = "{path}.{ext}.{lang}" - else: - # use v7 default there - self.config['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN - - # HIDE_SOURCELINK has been replaced with the inverted SHOW_SOURCELINK - # TODO: remove on v8 - if 'HIDE_SOURCELINK' in config: - utils.LOGGER.warn('The HIDE_SOURCELINK option is deprecated, use SHOW_SOURCELINK instead.') - if 'SHOW_SOURCELINK' in config: - utils.LOGGER.warn('HIDE_SOURCELINK conflicts with SHOW_SOURCELINK, ignoring HIDE_SOURCELINK.') - self.config['SHOW_SOURCELINK'] = not config['HIDE_SOURCELINK'] - - # HIDE_UNTRANSLATED_POSTS has been replaced with the inverted SHOW_UNTRANSLATED_POSTS - # TODO: remove on v8 - if 'HIDE_UNTRANSLATED_POSTS' in config: - utils.LOGGER.warn('The HIDE_UNTRANSLATED_POSTS option is deprecated, use SHOW_UNTRANSLATED_POSTS instead.') - if 'SHOW_UNTRANSLATED_POSTS' in config: - utils.LOGGER.warn('HIDE_UNTRANSLATED_POSTS conflicts with SHOW_UNTRANSLATED_POSTS, ignoring HIDE_UNTRANSLATED_POSTS.') - self.config['SHOW_UNTRANSLATED_POSTS'] = not config['HIDE_UNTRANSLATED_POSTS'] - - # READ_MORE_LINK has been split into INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK - # TODO: remove on v8 - if 'READ_MORE_LINK' in config: - utils.LOGGER.warn('The READ_MORE_LINK option is deprecated, use INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK instead.') - if 'INDEX_READ_MORE_LINK' in config: - utils.LOGGER.warn('READ_MORE_LINK conflicts with INDEX_READ_MORE_LINK, ignoring READ_MORE_LINK.') - else: - self.config['INDEX_READ_MORE_LINK'] = utils.TranslatableSetting('INDEX_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS']) - - if 'RSS_READ_MORE_LINK' in config: - utils.LOGGER.warn('READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring READ_MORE_LINK.') + # Handle old plugin names (from before merging the taxonomy PR #2535) + for old_plugin_name, new_plugin_names in TAXONOMY_COMPATIBILITY_PLUGIN_NAME_MAP.items(): + if old_plugin_name in self.config['DISABLED_PLUGINS']: + missing_plugins = [] + for plugin_name in new_plugin_names: + if plugin_name not in self.config['DISABLED_PLUGINS']: + missing_plugins.append(plugin_name) + if missing_plugins: + utils.LOGGER.warning('The "{}" plugin was replaced by several taxonomy plugins (see PR #2535): {}'.format(old_plugin_name, ', '.join(new_plugin_names))) + utils.LOGGER.warning('You are currently disabling "{}", but not the following new taxonomy plugins: {}'.format(old_plugin_name, ', '.join(missing_plugins))) + utils.LOGGER.warning('Please also disable these new plugins or remove "{}" from the DISABLED_PLUGINS list.'.format(old_plugin_name)) + self.config['DISABLED_PLUGINS'].extend(missing_plugins) + # Special-case logic for "render_indexes" to fix #2591 + if 'render_indexes' in self.config['DISABLED_PLUGINS']: + if 'generate_rss' in self.config['DISABLED_PLUGINS'] or self.config['GENERATE_RSS'] is False: + if 'classify_indexes' not in self.config['DISABLED_PLUGINS']: + utils.LOGGER.warning('You are disabling the "render_indexes" plugin, as well as disabling the "generate_rss" plugin or setting GENERATE_RSS to False. To achieve the same effect, please disable the "classify_indexes" plugin in the future.') + self.config['DISABLED_PLUGINS'].append('classify_indexes') else: - self.config['RSS_READ_MORE_LINK'] = utils.TranslatableSetting('RSS_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS']) - - # Moot.it renamed themselves to muut.io - # TODO: remove on v8? - if self.config.get('COMMENT_SYSTEM') == 'moot': - utils.LOGGER.warn('The moot comment system has been renamed to muut by the upstream. Setting COMMENT_SYSTEM to "muut".') - self.config['COMMENT_SYSTEM'] = 'muut' + if not self.config['DISABLE_INDEXES']: + utils.LOGGER.warning('You are disabling the "render_indexes" plugin, but not the generation of RSS feeds. Please put "DISABLE_INDEXES = True" into your configuration instead.') + self.config['DISABLE_INDEXES'] = True # Disable RSS. For a successful disable, we must have both the option # false and the plugin disabled through the official means. if 'generate_rss' in self.config['DISABLED_PLUGINS'] and self.config['GENERATE_RSS'] is True: + utils.LOGGER.warning('Please use GENERATE_RSS to disable RSS feed generation, instead of mentioning generate_rss in DISABLED_PLUGINS.') self.config['GENERATE_RSS'] = False - - if not self.config['GENERATE_RSS'] and 'generate_rss' not in self.config['DISABLED_PLUGINS']: - self.config['DISABLED_PLUGINS'].append('generate_rss') + self.config['DISABLE_MAIN_RSS_FEED'] = True # PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled if self.config.get('PRETTY_URLS') and 'STRIP_INDEXES' not in config: self.config['STRIP_INDEXES'] = True - if 'LISTINGS_FOLDER' in config: - if 'LISTINGS_FOLDERS' not in config: - utils.LOGGER.warn("The LISTINGS_FOLDER option is deprecated, use LISTINGS_FOLDERS instead.") - self.config['LISTINGS_FOLDERS'] = {self.config['LISTINGS_FOLDER']: self.config['LISTINGS_FOLDER']} - utils.LOGGER.warn("LISTINGS_FOLDERS = {0}".format(self.config['LISTINGS_FOLDERS'])) - else: - utils.LOGGER.warn("Both LISTINGS_FOLDER and LISTINGS_FOLDERS are specified, ignoring LISTINGS_FOLDER.") - - if 'GALLERY_PATH' in config: - if 'GALLERY_FOLDERS' not in config: - utils.LOGGER.warn("The GALLERY_PATH option is deprecated, use GALLERY_FOLDERS instead.") - self.config['GALLERY_FOLDERS'] = {self.config['GALLERY_PATH']: self.config['GALLERY_PATH']} - utils.LOGGER.warn("GALLERY_FOLDERS = {0}".format(self.config['GALLERY_FOLDERS'])) - else: - utils.LOGGER.warn("Both GALLERY_PATH and GALLERY_FOLDERS are specified, ignoring GALLERY_PATH.") - if not self.config.get('COPY_SOURCES'): self.config['SHOW_SOURCELINK'] = False @@ -833,18 +912,14 @@ class Nikola(object): self.default_lang = self.config['DEFAULT_LANG'] self.translations = self.config['TRANSLATIONS'] - locale_fallback, locale_default, locales = sanitized_locales( - self.config.get('LOCALE_FALLBACK', None), - self.config.get('LOCALE_DEFAULT', None), - self.config.get('LOCALES', {}), self.translations) - utils.LocaleBorg.initialize(locales, self.default_lang) + utils.LocaleBorg.initialize(self.config.get('LOCALES', {}), self.default_lang) # BASE_URL defaults to SITE_URL if 'BASE_URL' not in self.config: self.config['BASE_URL'] = self.config.get('SITE_URL') # BASE_URL should *always* end in / if self.config['BASE_URL'] and self.config['BASE_URL'][-1] != '/': - utils.LOGGER.warn("Your BASE_URL doesn't end in / -- adding it, but please fix it in your config file!") + utils.LOGGER.warning("Your BASE_URL doesn't end in / -- adding it, but please fix it in your config file!") self.config['BASE_URL'] += '/' try: @@ -856,35 +931,26 @@ class Nikola(object): utils.LOGGER.error("Punycode of {}: {}".format(_bnl, _bnl.encode('idna'))) sys.exit(1) - # TODO: remove in v8 - if not isinstance(self.config['DEPLOY_COMMANDS'], dict): - utils.LOGGER.warn("A single list as DEPLOY_COMMANDS is deprecated. DEPLOY_COMMANDS should be a dict, with deploy preset names as keys and lists of commands as values.") - utils.LOGGER.warn("The key `default` is used by `nikola deploy`:") - self.config['DEPLOY_COMMANDS'] = {'default': self.config['DEPLOY_COMMANDS']} - utils.LOGGER.warn("DEPLOY_COMMANDS = {0}".format(self.config['DEPLOY_COMMANDS'])) - utils.LOGGER.info("(The above can be used with `nikola deploy` or `nikola deploy default`. Multiple presets are accepted.)") - - # TODO: remove and change default in v8 - if 'BLOG_TITLE' in config and 'WRITE_TAG_CLOUD' not in config: - # BLOG_TITLE is a hack, otherwise the warning would be displayed - # when conf.py does not exist - utils.LOGGER.warn("WRITE_TAG_CLOUD is not set in your config. Defaulting to True (== writing tag_cloud_data.json).") - utils.LOGGER.warn("Please explicitly add the setting to your conf.py with the desired value, as the setting will default to False in the future.") - - # Rename stories to pages (#1891, #2518) - # TODO: remove in v8 - if 'COMMENTS_IN_STORIES' in config: - utils.LOGGER.warn('The COMMENTS_IN_STORIES option is deprecated, use COMMENTS_IN_PAGES instead.') - self.config['COMMENTS_IN_PAGES'] = config['COMMENTS_IN_STORIES'] - if 'STORY_INDEX' in config: - utils.LOGGER.warn('The STORY_INDEX option is deprecated, use PAGE_INDEX instead.') - self.config['PAGE_INDEX'] = config['STORY_INDEX'] + # Load built-in metadata extractors + metadata_extractors.load_defaults(self, self.metadata_extractors_by) + if metadata_extractors.DEFAULT_EXTRACTOR is None: + utils.LOGGER.error("Could not find default meta extractor ({})".format( + metadata_extractors.DEFAULT_EXTRACTOR_NAME)) + sys.exit(1) + + # The Pelican metadata format requires a markdown extension + if config.get('METADATA_FORMAT', 'nikola').lower() == 'pelican': + if 'markdown.extensions.meta' not in config.get('MARKDOWN_EXTENSIONS', []) \ + and 'markdown' in self.config['COMPILERS']: + utils.LOGGER.warning( + 'To use the Pelican metadata format, you need to add ' + '"markdown.extensions.meta" to your MARKDOWN_EXTENSIONS setting.') # We use one global tzinfo object all over Nikola. try: self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE']) except Exception as exc: - utils.LOGGER.warn("Error getting TZ: {}", exc) + utils.LOGGER.warning("Error getting TZ: {}", exc) self.tzinfo = dateutil.tz.gettz() self.config['__tzinfo__'] = self.tzinfo @@ -893,33 +959,23 @@ class Nikola(object): 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) - # Get search path for themes self.themes_dirs = ['themes'] + self.config['EXTRA_THEMES_DIRS'] - # 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) + # Register default filters + filter_name_format = 'filters.{0}' + for filter_name, filter_definition in filters.__dict__.items(): + # Ignore objects whose name starts with an underscore, or which are not callable + if filter_name.startswith('_') or not callable(filter_definition): + continue + # Register all other objects as filters + self.register_filter(filter_name_format.format(filter_name), filter_definition) self._set_global_context_from_config() - self._set_global_context_from_data() + self._set_all_page_deps_from_config() + # Read data files only if a site exists (Issue #2708) + if self.configured: + self._set_global_context_from_data() # Set persistent state facility self.state = Persistor('state_data.json') @@ -932,6 +988,30 @@ class Nikola(object): self.state._set_site(self) self.cache._set_site(self) + def _filter_duplicate_plugins(self, plugin_list): + """Find repeated plugins and discard the less local copy.""" + def plugin_position_in_places(plugin): + # plugin here is a tuple: + # (path to the .plugin file, path to plugin module w/o .py, plugin metadata) + for i, place in enumerate(self._plugin_places): + if plugin[0].startswith(place): + return i + utils.LOGGER.warn("Duplicate plugin found in unexpected location: {}".format(plugin[0])) + return len(self._plugin_places) + + plugin_dict = defaultdict(list) + for data in plugin_list: + plugin_dict[data[2].name].append(data) + result = [] + for _, plugins in plugin_dict.items(): + if len(plugins) > 1: + # Sort by locality + plugins.sort(key=plugin_position_in_places) + utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format( + plugins[-1][2].name, plugins[-1][0])) + result.append(plugins[-1]) + return result + def init_plugins(self, commands_only=False, load_all=False): """Load plugins as needed.""" self.plugin_manager = PluginManager(categories_filter={ @@ -944,25 +1024,37 @@ class Nikola(object): "CompilerExtension": CompilerExtension, "MarkdownExtension": MarkdownExtension, "RestExtension": RestExtension, + "MetadataExtractor": MetadataExtractor, "ShortcodePlugin": ShortcodePlugin, "SignalHandler": SignalHandler, "ConfigPlugin": ConfigPlugin, "PostScanner": PostScanner, + "Taxonomy": Taxonomy, }) self.plugin_manager.getPluginLocator().setPluginInfoExtension('plugin') extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS'] - if sys.version_info[0] == 3: - self._plugin_places = [ - resource_filename('nikola', 'plugins'), - os.path.expanduser('~/.nikola/plugins'), - os.path.join(os.getcwd(), 'plugins'), - ] + [path for path in extra_plugins_dirs if path] - else: - self._plugin_places = [ - resource_filename('nikola', utils.sys_encode('plugins')), - os.path.join(os.getcwd(), utils.sys_encode('plugins')), - os.path.expanduser('~/.nikola/plugins'), - ] + [utils.sys_encode(path) for path in extra_plugins_dirs if path] + self._plugin_places = [ + resource_filename('nikola', 'plugins'), + os.path.expanduser(os.path.join('~', '.nikola', 'plugins')), + os.path.join(os.getcwd(), 'plugins'), + ] + [path for path in extra_plugins_dirs if path] + + 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 (if load_all is False): + # Remove compilers (and corresponding compiler extensions) that are not marked as + # needed by any PostScanner plugin and put them into self.disabled_compilers + # (respectively self.disabled_compiler_extensions). + self.config['COMPILERS'] = {} + self.disabled_compilers = {} + self.disabled_compiler_extensions = defaultdict(list) self.plugin_manager.getPluginLocator().setPluginPlaces(self._plugin_places) self.plugin_manager.locatePlugins() @@ -970,55 +1062,80 @@ class Nikola(object): if not load_all: for p in self.plugin_manager._candidates: if commands_only: - if p[-1].details.has_option('Nikola', 'plugincategory'): + 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: bad_candidates.add(p) elif self.configured: # Not commands-only, and configured - # 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 compilers we don't use + if p[-1].details.has_option('Nikola', 'PluginCategory') and p[-1].details.get('Nikola', 'PluginCategory') in ('Compiler', 'PageCompiler'): + bad_candidates.add(p) + self.disabled_compilers[p[-1].name] = p # 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 compiler extension {}', p[-1].name) + self.disabled_compiler_extensions[p[-1].details.get('Nikola', 'compiler')].append(p) self.plugin_manager._candidates = list(set(self.plugin_manager._candidates) - bad_candidates) - # Find repeated plugins and discard the less local copy - def plugin_position_in_places(plugin): - # plugin here is a tuple: - # (path to the .plugin file, path to plugin module w/o .py, plugin metadata) - for i, place in enumerate(self._plugin_places): - if plugin[0].startswith(place): - return i - - plugin_dict = defaultdict(list) - for data in self.plugin_manager._candidates: - plugin_dict[data[2].name].append(data) - self.plugin_manager._candidates = [] - for name, plugins in plugin_dict.items(): - if len(plugins) > 1: - # Sort by locality - plugins.sort(key=plugin_position_in_places) - utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format( - plugins[-1][2].name, plugins[-1][0])) - self.plugin_manager._candidates.append(plugins[-1]) - + self.plugin_manager._candidates = self._filter_duplicate_plugins(self.plugin_manager._candidates) self.plugin_manager.loadPlugins() + # Search for compiler plugins which we disabled but shouldn't have + self._activate_plugins_of_category("PostScanner") + if not load_all: + file_extensions = set() + for post_scanner in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('PostScanner')]: + exts = post_scanner.supported_extensions() + if exts is not None: + file_extensions.update(exts) + else: + # Stop scanning for more: once we get None, we have to load all compilers anyway + utils.LOGGER.debug("Post scanner {0!r} does not implement `supported_extensions`, loading all compilers".format(post_scanner)) + file_extensions = None + break + to_add = [] + for k, v in compilers.items(): + if file_extensions is None or file_extensions.intersection(v): + self.config['COMPILERS'][k] = sorted(list(v)) + p = self.disabled_compilers.pop(k, None) + if p: + to_add.append(p) + for p in self.disabled_compiler_extensions.pop(k, []): + to_add.append(p) + for _, p in self.disabled_compilers.items(): + utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name) + for _, plugins in self.disabled_compiler_extensions.items(): + for p in plugins: + utils.LOGGER.debug('Not loading compiler extension {}', p[-1].name) + if to_add: + self.plugin_manager._candidates = self._filter_duplicate_plugins(to_add) + self.plugin_manager.loadPlugins() + + # Jupyter theme configuration. If a website has ipynb enabled in post_pages + # we should enable the Jupyter CSS (leaving that up to the theme itself). + if 'needs_ipython_css' not in self._GLOBAL_CONTEXT: + self._GLOBAL_CONTEXT['needs_ipython_css'] = 'ipynb' in self.config['COMPILERS'] + + # Activate metadata extractors and prepare them for use + for p in self._activate_plugins_of_category("MetadataExtractor"): + metadata_extractors.classify_extractor(p.plugin_object, self.metadata_extractors_by) + + self._activate_plugins_of_category("Taxonomy") + self.taxonomy_plugins = {} + for taxonomy in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('Taxonomy')]: + if not taxonomy.is_enabled(): + continue + if taxonomy.classification_name in self.taxonomy_plugins: + utils.LOGGER.error("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name)) + sys.exit(1) + self.taxonomy_plugins[taxonomy.classification_name] = taxonomy + self._activate_plugins_of_category("SignalHandler") # Emit signal for SignalHandlers which need to start running immediately. @@ -1031,7 +1148,6 @@ class Nikola(object): plugin_info.plugin_object.short_help = plugin_info.description self._commands[plugin_info.name] = plugin_info.plugin_object - self._activate_plugins_of_category("PostScanner") self._activate_plugins_of_category("Task") self._activate_plugins_of_category("LateTask") self._activate_plugins_of_category("TaskMultiplier") @@ -1055,8 +1171,28 @@ class Nikola(object): self.compilers[plugin_info.name] = \ plugin_info.plugin_object + # Load config plugins and register templated shortcodes self._activate_plugins_of_category("ConfigPlugin") self._register_templated_shortcodes() + + # Check with registered filters and configure filters + for actions in self.config['FILTERS'].values(): + for i, f in enumerate(actions): + if isinstance(f, str): + # Check whether this denotes a registered filter + _f = self.filters.get(f) + if _f is not None: + f = _f + actions[i] = f + if hasattr(f, 'configuration_variables'): + args = {} + for arg, config in f.configuration_variables.items(): + if config in self.config: + args[arg] = self.config[config] + if args: + actions[i] = functools.partial(f, **args) + + # Signal that we are configured signal('configured').send(self) def _set_global_context_from_config(self): @@ -1075,30 +1211,24 @@ class Nikola(object): self._GLOBAL_CONTEXT['rel_link'] = self.rel_link self._GLOBAL_CONTEXT['abs_link'] = self.abs_link self._GLOBAL_CONTEXT['exists'] = self.file_exists - self._GLOBAL_CONTEXT['SLUG_AUTHOR_PATH'] = self.config['SLUG_AUTHOR_PATH'] - self._GLOBAL_CONTEXT['SLUG_TAG_PATH'] = self.config['SLUG_TAG_PATH'] - self._GLOBAL_CONTEXT['annotations'] = self.config['ANNOTATIONS'] self._GLOBAL_CONTEXT['index_display_post_count'] = self.config[ 'INDEX_DISPLAY_POST_COUNT'] self._GLOBAL_CONTEXT['index_file'] = self.config['INDEX_FILE'] - self._GLOBAL_CONTEXT['use_base_tag'] = self.config['USE_BASE_TAG'] self._GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES'] self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN") self._GLOBAL_CONTEXT['theme_color'] = self.config.get("THEME_COLOR") + self._GLOBAL_CONTEXT['theme_config'] = self.config.get("THEME_CONFIG") self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS'] self._GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT') self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR') self._GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE') + self._GLOBAL_CONTEXT['blog_email'] = self.config.get('BLOG_EMAIL') self._GLOBAL_CONTEXT['show_blog_title'] = self.config.get('SHOW_BLOG_TITLE') self._GLOBAL_CONTEXT['logo_url'] = self.config.get('LOGO_URL') self._GLOBAL_CONTEXT['blog_description'] = self.config.get('BLOG_DESCRIPTION') self._GLOBAL_CONTEXT['front_index_header'] = self.config.get('FRONT_INDEX_HEADER') self._GLOBAL_CONTEXT['color_hsl_adjust_hex'] = utils.color_hsl_adjust_hex self._GLOBAL_CONTEXT['colorize_str_from_base_color'] = utils.colorize_str_from_base_color - - # TODO: remove in v8 - self._GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION') - self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL') self._GLOBAL_CONTEXT['template_hooks'] = self.template_hooks self._GLOBAL_CONTEXT['body_end'] = self.config.get('BODY_END') @@ -1113,19 +1243,15 @@ class Nikola(object): 'MATHJAX_CONFIG') self._GLOBAL_CONTEXT['use_katex'] = self.config.get('USE_KATEX') self._GLOBAL_CONTEXT['katex_auto_render'] = self.config.get('KATEX_AUTO_RENDER') - self._GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONFIG_SUBTHEME') - self._GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONFIG_TRANSITION') self._GLOBAL_CONTEXT['content_footer'] = self.config.get( 'CONTENT_FOOTER') self._GLOBAL_CONTEXT['generate_atom'] = self.config.get('GENERATE_ATOM') self._GLOBAL_CONTEXT['generate_rss'] = self.config.get('GENERATE_RSS') - self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH') self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK') self._GLOBAL_CONTEXT['navigation_links'] = self.config.get('NAVIGATION_LINKS') + self._GLOBAL_CONTEXT['navigation_alt_links'] = self.config.get('NAVIGATION_ALT_LINKS') - self._GLOBAL_CONTEXT['use_open_graph'] = self.config.get( - 'USE_OPEN_GRAPH', True) self._GLOBAL_CONTEXT['twitter_card'] = self.config.get( 'TWITTER_CARD', {}) self._GLOBAL_CONTEXT['hide_sourcelink'] = not self.config.get( @@ -1134,25 +1260,24 @@ class Nikola(object): 'SHOW_SOURCELINK') self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA') self._GLOBAL_CONTEXT['date_fanciness'] = self.config.get('DATE_FANCINESS') - self._GLOBAL_CONTEXT['js_date_format'] = self.config.get('JS_DATE_FORMAT') - self._GLOBAL_CONTEXT['colorbox_locales'] = LEGAL_VALUES['COLORBOX_LOCALES'] + self._GLOBAL_CONTEXT['luxon_locales'] = LEGAL_VALUES['LUXON_LOCALES'] + self._GLOBAL_CONTEXT['luxon_date_format'] = self.config.get('LUXON_DATE_FORMAT') + # TODO: remove in v9 + self._GLOBAL_CONTEXT['js_date_format'] = self.config.get('MOMENTJS_DATE_FORMAT') self._GLOBAL_CONTEXT['momentjs_locales'] = LEGAL_VALUES['MOMENTJS_LOCALES'] + # Patch missing locales into momentjs defaulting to English (Issue #3216) + for l in self._GLOBAL_CONTEXT['translations']: + if l not in self._GLOBAL_CONTEXT['momentjs_locales']: + self._GLOBAL_CONTEXT['momentjs_locales'][l] = "" self._GLOBAL_CONTEXT['hidden_tags'] = self.config.get('HIDDEN_TAGS') self._GLOBAL_CONTEXT['hidden_categories'] = self.config.get('HIDDEN_CATEGORIES') self._GLOBAL_CONTEXT['hidden_authors'] = self.config.get('HIDDEN_AUTHORS') self._GLOBAL_CONTEXT['url_replacer'] = self.url_replacer - self._GLOBAL_CONTEXT['posts_sections'] = self.config.get('POSTS_SECTIONS') - self._GLOBAL_CONTEXT['posts_section_are_indexes'] = self.config.get('POSTS_SECTION_ARE_INDEXES') - self._GLOBAL_CONTEXT['posts_section_colors'] = self.config.get('POSTS_SECTION_COLORS') - self._GLOBAL_CONTEXT['posts_section_descriptions'] = self.config.get('POSTS_SECTION_DESCRIPTIONS') - self._GLOBAL_CONTEXT['posts_section_from_meta'] = self.config.get('POSTS_SECTION_FROM_META') - self._GLOBAL_CONTEXT['posts_section_name'] = self.config.get('POSTS_SECTION_NAME') - self._GLOBAL_CONTEXT['posts_section_title'] = self.config.get('POSTS_SECTION_TITLE') - - # 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'] = 'ipynb' in self.config['COMPILERS'] + self._GLOBAL_CONTEXT['sort_posts'] = utils.sort_posts + self._GLOBAL_CONTEXT['smartjoin'] = utils.smartjoin + self._GLOBAL_CONTEXT['colorize_str'] = utils.colorize_str + self._GLOBAL_CONTEXT['meta_generator_tag'] = self.config.get('META_GENERATOR_TAG') + self._GLOBAL_CONTEXT['multiple_authors_per_post'] = self.config.get('MULTIPLE_AUTHORS_PER_POST') self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {})) @@ -1165,6 +1290,24 @@ class Nikola(object): data = utils.load_data(fname) key = os.path.splitext(fname.split(os.sep, 1)[1])[0] self._GLOBAL_CONTEXT['data'][key] = data + # Offer global_data as an alias for data (Issue #2488) + self._GLOBAL_CONTEXT['global_data'] = self._GLOBAL_CONTEXT['data'] + + def _set_all_page_deps_from_config(self): + """Save dependencies for all pages from configuration. + + Changes of values in this dict will force a rebuild of all pages. + Unlike global context, contents are NOT available to templates. + """ + self.ALL_PAGE_DEPS['atom_extension'] = self.config.get('ATOM_EXTENSION') + self.ALL_PAGE_DEPS['atom_path'] = self.config.get('ATOM_PATH') + self.ALL_PAGE_DEPS['rss_extension'] = self.config.get('RSS_EXTENSION') + self.ALL_PAGE_DEPS['rss_path'] = self.config.get('RSS_PATH') + self.ALL_PAGE_DEPS['rss_filename_base'] = self.config.get('RSS_FILENAME_BASE') + self.ALL_PAGE_DEPS['atom_filename_base'] = self.config.get('ATOM_FILENAME_BASE') + self.ALL_PAGE_DEPS['slug_author_path'] = self.config.get('SLUG_AUTHOR_PATH') + self.ALL_PAGE_DEPS['slug_tag_path'] = self.config.get('SLUG_TAG_PATH') + self.ALL_PAGE_DEPS['locale'] = self.config.get('LOCALE') def _activate_plugins_of_category(self, category): """Activate all the plugins of a given category and return them.""" @@ -1181,17 +1324,18 @@ class Nikola(object): try: self._THEMES = utils.get_theme_chain(self.config['THEME'], self.themes_dirs) except Exception: - if self.config['THEME'] != 'bootstrap3': - utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap3' instead.'''.format(self.config['THEME'])) - self.config['THEME'] = 'bootstrap3' + if self.config['THEME'] != LEGAL_VALUES['DEFAULT_THEME']: + utils.LOGGER.warning('''Cannot load theme "{0}", using '{1}' instead.'''.format( + self.config['THEME'], LEGAL_VALUES['DEFAULT_THEME'])) + self.config['THEME'] = LEGAL_VALUES['DEFAULT_THEME'] return self._get_themes() raise # Check consistency of USE_CDN and the current THEME (Issue #386) if self.config['USE_CDN'] and self.config['USE_CDN_WARNING']: bootstrap_path = utils.get_asset_path(os.path.join( 'assets', 'css', 'bootstrap.min.css'), self._THEMES) - if bootstrap_path and bootstrap_path.split(os.sep)[-4] not in ['bootstrap', 'bootstrap3']: - utils.LOGGER.warn('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.') + if bootstrap_path and bootstrap_path.split(os.sep)[-4] not in ['bootstrap', 'bootstrap3', 'bootstrap4']: + utils.LOGGER.warning('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.') return self._THEMES @@ -1260,7 +1404,7 @@ class Nikola(object): """ ext = os.path.splitext(source_name)[1] try: - compile_html = self.inverse_compilers[ext] + compiler = self.inverse_compilers[ext] except KeyError: # Find the correct compiler for this files extension lang_exts_tab = list(self.config['COMPILERS'].items()) @@ -1268,26 +1412,28 @@ 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 " - "one of {0})".format(', '.join(langs))) + sys.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] else: - exit("COMPILERS in conf.py does not tell me how to " - "handle '{0}' extensions.".format(ext)) + sys.exit("COMPILERS in conf.py does not tell me how to " + "handle '{0}' extensions.".format(ext)) lang = langs[0] try: - compile_html = self.compilers[lang] + compiler = self.compilers[lang] except KeyError: - exit("Cannot find '{0}' compiler; it might require an extra plugin -- do you have it installed?".format(lang)) - self.inverse_compilers[ext] = compile_html + sys.exit("Cannot find '{0}' compiler; " + "it might require an extra plugin -- " + "do you have it installed?".format(lang)) + self.inverse_compilers[ext] = compiler - return compile_html + return compiler - def render_template(self, template_name, output_name, context, url_type=None): + def render_template(self, template_name, output_name, context, url_type=None, is_fragment=False): """Render a template with the global context. If ``output_name`` is None, will return a string and all URL @@ -1298,6 +1444,9 @@ class Nikola(object): The argument ``url_type`` allows to override the ``URL_TYPE`` configuration. + + If ``is_fragment`` is set to ``True``, a HTML fragment will + be rendered and not a whole HTML document. """ local_context = {} local_context["template_name"] = template_name @@ -1306,6 +1455,11 @@ class Nikola(object): for k in self._GLOBAL_CONTEXT_TRANSLATABLE: local_context[k] = local_context[k](local_context['lang']) local_context['is_rtl'] = local_context['lang'] in LEGAL_VALUES['RTL_LANGUAGES'] + local_context['url_type'] = self.config['URL_TYPE'] if url_type is None else url_type + local_context["translations_feedorder"] = sorted( + local_context["translations"], + key=lambda x: (int(x != local_context['lang']), x) + ) # string, arguments local_context["formatmsg"] = lambda s, *a: s % a for h in local_context['template_hooks'].values(): @@ -1320,8 +1474,8 @@ class Nikola(object): if output_name is None: return data - assert output_name.startswith( - self.config["OUTPUT_FOLDER"]) + if not output_name.startswith(self.config["OUTPUT_FOLDER"]): + raise ValueError("Output path for templates must start with OUTPUT_FOLDER") url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:] # Treat our site as if output/ is "/" and then make all URLs relative, @@ -1333,9 +1487,18 @@ 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) + if is_fragment: + doc = lxml.html.fragment_fromstring(data.strip(), parser) + else: + doc = lxml.html.document_fromstring(data.strip(), parser) self.rewrite_links(doc, src, context['lang'], url_type) - data = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True) + if is_fragment: + # doc.text contains text before the first HTML, or None if there was no text + # The text after HTML elements is added by tostring() (because its implicit + # argument with_tail has default value True). + data = (doc.text or '').encode('utf-8') + b''.join([lxml.html.tostring(child, encoding='utf-8', method='html') for child in doc.iterchildren()]) + else: + data = lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True, doctype='<!DOCTYPE html>') with open(output_name, "wb+") as post_file: post_file.write(data) @@ -1345,7 +1508,7 @@ class Nikola(object): doc.rewrite_links(lambda dst: self.url_replacer(src, dst, lang, url_type), resolve_base_href=False) # lxml ignores srcset in img and source elements, so do that by hand - objs = list(doc.xpath('(*//img|*//source)')) + objs = list(doc.xpath('(//img|//source)')) for obj in objs: if 'srcset' in obj.attrib: urls = [u.strip() for u in obj.attrib['srcset'].split(',')] @@ -1366,6 +1529,10 @@ class Nikola(object): lang is used for language-sensitive URLs in link:// url_type is used to determine final link appearance, defaulting to URL_TYPE from config """ + # Avoid mangling links within the page + if dst.startswith('#'): + return dst + parsed_src = urlsplit(src) src_elems = parsed_src.path.split('/')[1:] dst_url = urlparse(dst) @@ -1380,7 +1547,17 @@ class Nikola(object): # Refuse to replace links that are full URLs. if dst_url.netloc: if dst_url.scheme == 'link': # Magic link - dst = self.link(dst_url.netloc, dst_url.path.lstrip('/'), lang) + if dst_url.query: + # If query strings are used in magic link, they will be + # passed to the path handler as keyword arguments (strings) + link_kwargs = {unquote(k): unquote(v[-1]) for k, v in parse_qs(dst_url.query).items()} + else: + link_kwargs = {} + + # unquote from issue #2934 + dst = self.link(dst_url.netloc, unquote(dst_url.path.lstrip('/')), lang, **link_kwargs) + if dst_url.fragment: + dst += '#' + dst_url.fragment # Assuming the site is served over one of these, and # since those are the only URLs we want to rewrite... else: @@ -1393,7 +1570,7 @@ class Nikola(object): # python 3: already unicode pass nl = nl.encode('idna') - if isinstance(nl, utils.bytes_str): + if isinstance(nl, bytes): nl = nl.decode('latin-1') # so idna stays unchanged dst = urlunsplit((dst_url.scheme, nl, @@ -1421,7 +1598,7 @@ class Nikola(object): return dst elif url_type == 'full_path': dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) - return urlparse(dst).path + return utils.full_path_from_urlparse(urlparse(dst)) else: return "#" @@ -1436,10 +1613,7 @@ class Nikola(object): dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) if url_type == 'full_path': parsed = urlparse(urljoin(self.config['BASE_URL'], dst.lstrip('/'))) - if parsed.fragment: - dst = '{0}#{1}'.format(parsed.path, parsed.fragment) - else: - dst = parsed.path + dst = utils.full_path_from_urlparse(parsed) return dst # Now both paths are on the same site and absolute @@ -1463,7 +1637,8 @@ class Nikola(object): if parsed_dst.fragment: result += "#" + parsed_dst.fragment - assert result, (src, dst, i, src_elems, dst_elems) + if not result: + raise ValueError("Failed to parse link: {0}".format((src, dst, i, src_elems, dst_elems))) return result @@ -1476,6 +1651,12 @@ class Nikola(object): keyword argument dict and then the latter provides the template context. + Global context keys are made available as part of the context, + respecting locale. + + As a special quirk, the "data" key from global_context is + available only as "global_data" because of name clobbering. + """ def render_shortcode(*args, **kw): context = self.GLOBAL_CONTEXT.copy() @@ -1533,30 +1714,58 @@ class Nikola(object): def register_shortcode(self, name, f): """Register function f to handle shortcode "name".""" if name in self.shortcode_registry: - utils.LOGGER.warn('Shortcode name conflict: {}', name) + utils.LOGGER.warning('Shortcode name conflict: {}', name) return self.shortcode_registry[name] = f - # XXX in v8, get rid of with_dependencies - def apply_shortcodes(self, data, filename=None, lang=None, with_dependencies=False, extra_context={}): + def apply_shortcodes(self, data, filename=None, lang=None, extra_context=None): """Apply shortcodes from the registry on data.""" + if extra_context is None: + extra_context = {} if lang is None: lang = utils.LocaleBorg().current_lang - return shortcodes.apply_shortcodes(data, self.shortcode_registry, self, filename, lang=lang, with_dependencies=with_dependencies, extra_context=extra_context) + return shortcodes.apply_shortcodes(data, self.shortcode_registry, self, filename, lang=lang, extra_context=extra_context) - 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): - """Take all necessary data, and render a RSS feed in output_path.""" + def apply_shortcodes_uuid(self, data, _shortcodes, filename=None, lang=None, extra_context=None): + """Apply shortcodes from the registry on data.""" + if lang is None: + lang = utils.LocaleBorg().current_lang + if extra_context is None: + extra_context = {} + deps = [] + for k, v in _shortcodes.items(): + replacement, _deps = shortcodes.apply_shortcodes(v, self.shortcode_registry, self, filename, lang=lang, extra_context=extra_context) + data = data.replace(k, replacement) + deps.extend(_deps) + return data, deps + + def _get_rss_copyright(self, lang, rss_plain): + if rss_plain: + return ( + self.config['RSS_COPYRIGHT_PLAIN'](lang) or + lxml.html.fromstring(self.config['RSS_COPYRIGHT'](lang)).text_content().strip()) + else: + return self.config['RSS_COPYRIGHT'](lang) + + def generic_rss_feed(self, lang, title, link, description, timeline, + rss_teasers, rss_plain, feed_length=10, feed_url=None, + enclosure=_enclosure, rss_links_append_query=None, copyright_=None): + """Generate an ExtendedRSS2 feed object for later use.""" rss_obj = utils.ExtendedRSS2( title=title, link=utils.encodelink(link), description=description, lastBuildDate=datetime.datetime.utcnow(), - generator='https://getnikola.com/', + generator='Nikola (getnikola.com)', language=lang ) + if copyright_ is None: + copyright_ = self._get_rss_copyright(lang, rss_plain) + # Use the configured or specified copyright string if present. + if copyright_: + rss_obj.copyright = copyright_ + if feed_url: absurl = '/' + feed_url[len(self.config['BASE_URL']):] rss_obj.xsl_stylesheet_href = self.url_replacer(absurl, "/assets/xml/rss.xsl") @@ -1565,6 +1774,8 @@ class Nikola(object): feed_append_query = None if rss_links_append_query: + if rss_links_append_query is True: + raise ValueError("RSS_LINKS_APPEND_QUERY (or FEED_LINKS_APPEND_QUERY) cannot be True. Valid values are False or a formattable string.") feed_append_query = rss_links_append_query.format( feedRelUri='/' + feed_url[len(self.config['BASE_URL']):], feedFormat="rss") @@ -1575,7 +1786,7 @@ class Nikola(object): if feed_url is not None and data: # Massage the post's HTML (unless plain) if not rss_plain: - if self.config["FEED_PREVIEWIMAGE"] and 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in data: + if 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in data: data = "<figure><img src=\"{}\"></figure> {}".format(post.meta[lang]['previewimage'], data) # FIXME: this is duplicated with code in Post.text() try: @@ -1592,9 +1803,9 @@ class Nikola(object): if str(e) == "Document is empty": data = "" else: # let other errors raise - raise(e) + raise args = { - 'title': post.title(lang), + 'title': post.title(lang) if post.should_show_title() else None, 'link': post.permalink(lang, absolute=True, query=feed_append_query), 'description': data, # PyRSS2Gen's pubDate is GMT time. @@ -1602,7 +1813,7 @@ class Nikola(object): post.date.astimezone(dateutil.tz.tzutc())), 'categories': post._tags.get(lang, []), 'creator': post.author(lang), - 'guid': post.permalink(lang, absolute=True), + 'guid': post.guid(lang), } if post.author(lang): @@ -1620,16 +1831,18 @@ class Nikola(object): rss_obj.items = items rss_obj.self_url = feed_url rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" + return rss_obj - dst_dir = os.path.dirname(output_path) - utils.makedirs(dst_dir) - with io.open(output_path, "w+", encoding="utf-8") as rss_file: - data = rss_obj.to_xml(encoding='utf-8') - if isinstance(data, utils.bytes_str): - data = data.decode('utf-8') - rss_file.write(data) + def 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, copyright_=None): + """Take all necessary data, and render a RSS feed in output_path.""" + rss_obj = self.generic_rss_feed(lang, title, link, description, timeline, + rss_teasers, rss_plain, feed_length=feed_length, feed_url=feed_url, + enclosure=enclosure, rss_links_append_query=rss_links_append_query, copyright_=copyright_) + utils.rss_writer(rss_obj, output_path) - def path(self, kind, name, lang=None, is_link=False): + def path(self, kind, name, lang=None, is_link=False, **kwargs): r"""Build the path to a certain kind of page. These are mostly defined by plugins by registering via the @@ -1653,35 +1866,47 @@ class Nikola(object): * slug (name is the slug of a post or page) * filename (name is the source filename of a post/page, in DEFAULT_LANG, relative to conf.py) - The returned value is always a path relative to output, like - "categories/whatever.html" + The returned value is either a path relative to output, like "categories/whatever.html", or + an absolute URL ("https://getnikola.com/"), if path handler returns a string. If is_link is True, the path is absolute and uses "/" as separator (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") + If the registered path handler returns a string instead of path component list - it's + considered to be an absolute URL and returned as is. + """ if lang is None: lang = utils.LocaleBorg().current_lang try: - path = self.path_handlers[kind](name, lang) - path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028 - if is_link: - link = '/' + ('/'.join(path)) - index_len = len(self.config['INDEX_FILE']) - if self.config['STRIP_INDEXES'] and \ - link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']: - return link[:-index_len] - else: - return link - else: - return os.path.join(*path) + path = self.path_handlers[kind](name, lang, **kwargs) except KeyError: - utils.LOGGER.warn("Unknown path request of kind: {0}".format(kind)) + utils.LOGGER.warning("Unknown path request of kind: {0}".format(kind)) return "" + # If path handler returns a string we consider it to be an absolute URL not requiring any + # further processing, i.e 'https://getnikola.com/'. See Issue #2876. + if isinstance(path, str): + return path + + if path is None: + path = "#" + else: + path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028 + if is_link: + link = '/' + ('/'.join(path)) + index_len = len(self.config['INDEX_FILE']) + if self.config['STRIP_INDEXES'] and \ + link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']: + return link[:-index_len] + else: + return link + else: + return os.path.join(*path) + def post_path(self, name, lang): """Link to the destination of an element in the POSTS/PAGES settings. @@ -1709,7 +1934,7 @@ class Nikola(object): return [] def slug_path(self, name, lang): - """A link to a post with given slug, if not ambiguous. + """Return a link to a post with given slug, if not ambiguous. Example: @@ -1721,7 +1946,7 @@ class Nikola(object): else: if len(results) > 1: utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name)) - return [_f for _f in results[0].permalink(lang).split('/') if _f] + return [_f for _f in results[0].permalink(lang).split('/')] def filename_path(self, name, lang): """Link to post or page by source filename. @@ -1745,9 +1970,9 @@ class Nikola(object): else: self.path_handlers[kind] = f - def link(self, *args): + def link(self, *args, **kwargs): """Create a link.""" - url = self.path(*args, is_link=True) + url = self.path(*args, is_link=True, **kwargs) url = utils.encodelink(url) return url @@ -1791,6 +2016,17 @@ class Nikola(object): url = utils.encodelink(url) return url + def register_filter(self, filter_name, filter_definition): + """Register a filter. + + filter_name should be a name not confusable with an actual + executable. filter_definition should be a callable accepting + one argument (the filename). + """ + if filter_name in self.filters: + utils.LOGGER.warning('''The filter "{0}" is defined more than once.'''.format(filter_name)) + self.filters[filter_name] = filter_definition + def file_exists(self, path, not_empty=False): """Check if the file exists. If not_empty is True, it also must not be empty.""" exists = os.path.exists(path) @@ -1819,7 +2055,8 @@ class Nikola(object): task_dep = [] for pluginInfo in self.plugin_manager.getPluginsOfCategory(plugin_category): for task in flatten(pluginInfo.plugin_object.gen_tasks()): - assert 'basename' in task + if 'basename' not in task: + raise ValueError("Task {0} does not have a basename".format(task)) task = self.clean_task_paths(task) if 'task_dep' not in task: task['task_dep'] = [] @@ -1846,7 +2083,7 @@ class Nikola(object): """Parse a category name into a hierarchy.""" if self.config['CATEGORY_ALLOW_HIERARCHIES']: try: - return utils.parse_escaped_hierarchical_category_name(category_name) + return hierarchy_utils.parse_escaped_hierarchical_category_name(category_name) except Exception as e: utils.LOGGER.error(str(e)) sys.exit(1) @@ -1856,7 +2093,7 @@ class Nikola(object): 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) + return hierarchy_utils.join_hierarchical_category_path(category_path) else: return ''.join(category_path) @@ -1881,7 +2118,7 @@ class Nikola(object): """Create category hierarchy.""" result = [] for name, children in cat_hierarchy.items(): - node = utils.TreeNode(name, parent) + node = hierarchy_utils.TreeNode(name, parent) node.children = create_hierarchy(children, node) node.category_path = [pn.name for pn in node.get_path()] node.category_name = self.category_path_to_category_name(node.category_path) @@ -1892,7 +2129,25 @@ class Nikola(object): root_list = create_hierarchy(self.category_hierarchy) # Next, flatten the hierarchy - self.category_hierarchy = utils.flatten_tree_structure(root_list) + self.category_hierarchy = hierarchy_utils.flatten_tree_structure(root_list) + + @staticmethod + def sort_posts_chronologically(posts, lang=None): + """Sort a list of posts chronologically. + + This function also takes priority, title and source path into account. + """ + # Last tie breaker: sort by source path (A-Z) + posts = sorted(posts, key=lambda p: p.source_path) + # Next tie breaker: sort by title if language is given (A-Z) + if lang is not None: + posts = natsort.natsorted(posts, key=lambda p: p.title(lang), alg=natsort.ns.F | natsort.ns.IC) + # Next tie breaker: sort by date (reverse chronological order) + posts = sorted(posts, key=lambda p: p.date, reverse=True) + # Finally, sort by priority meta value (descending) + posts = sorted(posts, key=lambda p: int(p.meta('priority')) if p.meta('priority') else 0, reverse=True) + # Return result + return posts def scan_posts(self, really=False, ignore_quit=False, quiet=False): """Scan all the posts. @@ -1916,8 +2171,12 @@ class Nikola(object): self.timeline = [] self.pages = [] - for p in self.plugin_manager.getPluginsOfCategory('PostScanner'): - timeline = p.plugin_object.scan() + for p in sorted(self.plugin_manager.getPluginsOfCategory('PostScanner'), key=operator.attrgetter('name')): + try: + timeline = p.plugin_object.scan() + except Exception: + utils.LOGGER.error('Error reading timeline') + raise # FIXME: can there be conflicts here? self.timeline.extend(timeline) @@ -1933,16 +2192,7 @@ class Nikola(object): for lang in self.config['TRANSLATIONS'].keys(): for tag in post.tags_for_language(lang): _tag_slugified = utils.slugify(tag, lang) - if _tag_slugified in slugged_tags[lang]: - if tag not in self.posts_per_tag: - # Tags that differ only in case - other_tag = [existing for existing in self.posts_per_tag.keys() if utils.slugify(existing, lang) == _tag_slugified][0] - utils.LOGGER.error('You have tags that are too similar: {0} and {1}'.format(tag, other_tag)) - utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path)) - utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join([p.source_path for p in self.posts_per_tag[other_tag]]))) - quit = True - else: - slugged_tags[lang].add(_tag_slugified) + slugged_tags[lang].add(_tag_slugified) if post not in self.posts_per_tag[tag]: self.posts_per_tag[tag].append(post) self.tags_per_language[lang].extend(post.tags_for_language(lang)) @@ -1972,17 +2222,17 @@ class Nikola(object): quit = True self.post_per_file[dest] = post self.post_per_file[src_dest] = post - self.post_per_input_file[src_file] = post + if src_file is not None: + self.post_per_input_file[src_file] = post # deduplicate tags_per_language self.tags_per_language[lang] = list(set(self.tags_per_language[lang])) # Sort everything. - for thing in self.timeline, self.posts, self.all_posts, self.pages: - thing.sort(key=lambda p: - (int(p.meta('priority')) if p.meta('priority') else 0, - p.date, p.source_path)) - thing.reverse() + self.timeline = self.sort_posts_chronologically(self.timeline) + self.posts = self.sort_posts_chronologically(self.posts) + self.all_posts = self.sort_posts_chronologically(self.all_posts) + self.pages = self.sort_posts_chronologically(self.pages) self._sort_category_hierarchy() for i, p in enumerate(self.posts[1:]): @@ -1996,8 +2246,8 @@ class Nikola(object): sys.exit(1) signal('scanned').send(self) - def generic_renderer(self, lang, output_name, template_name, filters, file_deps=None, uptodate_deps=None, context=None, context_deps_remove=None, post_deps_dict=None, url_type=None): - """Helper function for rendering pages and post lists and other related pages. + def generic_renderer(self, lang, output_name, template_name, filters, file_deps=None, uptodate_deps=None, context=None, context_deps_remove=None, post_deps_dict=None, url_type=None, is_fragment=False): + """Create tasks for rendering pages and post lists and other related pages. lang is the current language. output_name is the destination file name. @@ -2008,7 +2258,8 @@ class Nikola(object): context (optional) a dict used as a basis for the template context. The lang parameter will always be added. context_deps_remove (optional) is a list of keys to remove from the context after using it as an uptodate dependency. This should name all keys containing non-trivial Python objects; they can be replaced by adding JSON-style dicts in post_deps_dict. post_deps_dict (optional) is a dict merged into the copy of context which is used as an uptodate dependency. - url_type (optional) allows to override the ``URL_TYPE`` configuration + url_type (optional) allows to override the ``URL_TYPE`` configuration. + is_fragment (optional) allows to write a HTML fragment instead of a HTML document. """ utils.LocaleBorg().set_locale(lang) @@ -2026,23 +2277,27 @@ class Nikola(object): deps_dict['OUTPUT_FOLDER'] = self.config['OUTPUT_FOLDER'] deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS'] deps_dict['global'] = self.GLOBAL_CONTEXT + deps_dict['all_page_deps'] = self.ALL_PAGE_DEPS if post_deps_dict: deps_dict.update(post_deps_dict) for k, v in self.GLOBAL_CONTEXT['template_hooks'].items(): - deps_dict['||template_hooks|{0}||'.format(k)] = v._items + deps_dict['||template_hooks|{0}||'.format(k)] = v.calculate_deps() for k in self._GLOBAL_CONTEXT_TRANSLATABLE: deps_dict[k] = deps_dict['global'][k](lang) + for k in self._ALL_PAGE_DEPS_TRANSLATABLE: + deps_dict[k] = deps_dict['all_page_deps'][k](lang) deps_dict['navigation_links'] = deps_dict['global']['navigation_links'](lang) + deps_dict['navigation_alt_links'] = deps_dict['global']['navigation_alt_links'](lang) task = { 'name': os.path.normpath(output_name), 'targets': [output_name], 'file_dep': file_deps, 'actions': [(self.render_template, [template_name, output_name, - context, url_type])], + context, url_type, is_fragment])], 'clean': True, 'uptodate': [config_changed(deps_dict, 'nikola.nikola.Nikola.generic_renderer')] + ([] if uptodate_deps is None else uptodate_deps) } @@ -2051,19 +2306,30 @@ class Nikola(object): def generic_page_renderer(self, lang, post, filters, context=None): """Render post fragments to final HTML pages.""" - extension = self.get_compiler(post.source_path).extension() + extension = post.compiler.extension() output_name = os.path.join(self.config['OUTPUT_FOLDER'], post.destination_path(lang, extension)) deps = post.deps(lang) uptodate_deps = post.deps_uptodate(lang) deps.extend(utils.get_asset_path(x, self.THEMES) for x in ('bundles', 'parent', 'engine')) + _theme_ini = utils.get_asset_path(self.config['THEME'] + '.theme', self.THEMES) + if _theme_ini: + deps.append(_theme_ini) context = copy(context) if context else {} context['post'] = post context['title'] = post.title(lang) context['description'] = post.description(lang) context['permalink'] = post.permalink(lang) + if 'crumbs' not in context: + crumb_path = post.permalink(lang).lstrip('/') + if crumb_path.endswith(self.config['INDEX_FILE']): + crumb_path = crumb_path[:-len(self.config['INDEX_FILE'])] + if crumb_path.endswith('/'): + context['crumbs'] = utils.get_crumbs(crumb_path.rstrip('/'), is_file=False) + else: + context['crumbs'] = utils.get_crumbs(crumb_path, is_file=True) if 'pagekind' not in context: context['pagekind'] = ['generic_page'] if post.use_in_feeds: @@ -2080,12 +2346,21 @@ class Nikola(object): if post: deps_dict['post_translations'] = post.translated_to + signal('render_post').send({ + 'site': self, + 'post': post, + 'lang': lang, + 'context': context, + 'deps_dict': deps_dict, + }) + yield self.generic_renderer(lang, output_name, post.template_name, filters, file_deps=deps, uptodate_deps=uptodate_deps, context=context, context_deps_remove=['post'], - post_deps_dict=deps_dict) + post_deps_dict=deps_dict, + url_type=post.url_type) def generic_post_list_renderer(self, lang, posts, output_name, template_name, filters, extra_context): """Render pages with lists of posts.""" @@ -2103,6 +2378,8 @@ class Nikola(object): context["nextlink"] = None if extra_context: context.update(extra_context) + if 'has_other_languages' not in context: + context['has_other_languages'] = False post_deps_dict = {} post_deps_dict["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in posts] @@ -2132,30 +2409,30 @@ class Nikola(object): for post in posts: deps += post.deps(lang) uptodate_deps += post.deps_uptodate(lang) + context = {} + blog_title = self.config['BLOG_TITLE'](lang) context["posts"] = posts - context["title"] = self.config['BLOG_TITLE'](lang) + context["title"] = blog_title context["description"] = self.config['BLOG_DESCRIPTION'](lang) context["lang"] = lang - context["prevlink"] = None - context["nextlink"] = None - context["is_feed_stale"] = None context.update(extra_context) + + context["title"] = "{0} ({1})".format(blog_title, context["title"]) if blog_title != context["title"] else blog_title + deps_context = copy(context) deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in posts] deps_context["global"] = self.GLOBAL_CONTEXT + deps_context["all_page_deps"] = self.ALL_PAGE_DEPS for k in self._GLOBAL_CONTEXT_TRANSLATABLE: deps_context[k] = deps_context['global'][k](lang) + for k in self._ALL_PAGE_DEPS_TRANSLATABLE: + deps_context[k] = deps_context['all_page_deps'][k](lang) - deps_context['navigation_links'] = deps_context['global']['navigation_links'](lang) - - nslist = {} - if context["is_feed_stale"] or "feedpagenum" in context and (not context["feedpagenum"] == context["feedpagecount"] - 1 and not context["feedpagenum"] == 0): - nslist["fh"] = "http://purl.org/syndication/history/1.0" feed_xsl_link = self.abs_link("/assets/xml/atom.xsl") - feed_root = lxml.etree.Element("feed", nsmap=nslist) + feed_root = lxml.etree.Element("feed") feed_root.addprevious(lxml.etree.ProcessingInstruction( "xml-stylesheet", 'href="' + utils.encodelink(feed_xsl_link) + '" type="text/xsl media="all"')) @@ -2172,25 +2449,6 @@ class Nikola(object): feed_author_name.text = self.config["BLOG_AUTHOR"](lang) feed_root.append(atom_link("self", "application/atom+xml", self.abs_link(context["feedlink"]))) - # Older is "next" and newer is "previous" in paginated feeds (opposite of archived) - if "nextfeedlink" in context: - feed_root.append(atom_link("next", "application/atom+xml", - self.abs_link(context["nextfeedlink"]))) - if "prevfeedlink" in context: - feed_root.append(atom_link("previous", "application/atom+xml", - self.abs_link(context["prevfeedlink"]))) - if context["is_feed_stale"] or "feedpagenum" in context and not context["feedpagenum"] == 0: - feed_root.append(atom_link("current", "application/atom+xml", - self.abs_link(context["currentfeedlink"]))) - # Older is "prev-archive" and newer is "next-archive" in archived feeds (opposite of paginated) - if "prevfeedlink" in context and (context["is_feed_stale"] or "feedpagenum" in context and not context["feedpagenum"] == context["feedpagecount"] - 1): - feed_root.append(atom_link("next-archive", "application/atom+xml", - self.abs_link(context["prevfeedlink"]))) - if "nextfeedlink" in context: - feed_root.append(atom_link("prev-archive", "application/atom+xml", - self.abs_link(context["nextfeedlink"]))) - if context["is_feed_stale"] or "feedpagenum" and not context["feedpagenum"] == context["feedpagecount"] - 1: - lxml.etree.SubElement(feed_root, "{http://purl.org/syndication/history/1.0}archive") feed_root.append(atom_link("alternate", "text/html", self.abs_link(context["permalink"]))) feed_generator = lxml.etree.SubElement(feed_root, "generator") @@ -2205,7 +2463,7 @@ class Nikola(object): def atom_post_text(post, text): if not self.config["FEED_PLAIN"]: - if self.config["FEED_PREVIEWIMAGE"] and 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in text: + if 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in text: text = "<figure><img src=\"{}\"></figure> {}".format(post.meta[lang]['previewimage'], text) # FIXME: this is duplicated with code in Post.text() and generic_rss_renderer @@ -2223,7 +2481,7 @@ class Nikola(object): if str(e) == "Document is empty": text = "" else: # let other errors raise - raise(e) + raise return text.strip() for post in posts: @@ -2251,8 +2509,8 @@ class Nikola(object): entry_author_name = lxml.etree.SubElement(entry_author, "name") entry_author_name.text = post.author(lang) entry_root.append(atom_link("alternate", "text/html", - post.permalink(lang, absolute=True, - query=feed_append_query))) + post.permalink(lang, absolute=True, + query=feed_append_query))) entry_summary = lxml.etree.SubElement(entry_root, "summary") if not self.config["FEED_PLAIN"]: entry_summary.set("type", "html") @@ -2275,11 +2533,11 @@ class Nikola(object): utils.makedirs(dst_dir) with io.open(output_path, "w+", encoding="utf-8") as atom_file: data = lxml.etree.tostring(feed_root.getroottree(), encoding="UTF-8", pretty_print=True, xml_declaration=True) - if isinstance(data, utils.bytes_str): + if isinstance(data, bytes): data = data.decode('utf-8') 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=[]): + def generic_index_renderer(self, lang, posts, indexes_title, template_name, context_source, kw, basename, page_link, page_path, additional_dependencies=None): """Create an index page. lang: The language @@ -2304,6 +2562,9 @@ class Nikola(object): as the ones for page_link. additional_dependencies: a list of dependencies which will be added to task['uptodate'] + + Note: if context['featured'] is present, it must be a list of posts, + whose dependencies will be taken added to task['uptodate']. """ # Update kw kw = kw.copy() @@ -2313,11 +2574,11 @@ class Nikola(object): kw["indexes_pages"] = self.config['INDEXES_PAGES'](lang) kw["indexes_pages_main"] = self.config['INDEXES_PAGES_MAIN'] kw["indexes_static"] = self.config['INDEXES_STATIC'] - kw['indexes_prety_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"] - kw['demote_headers'] = self.config['DEMOTE_HEADERS'] - kw['generate_atom'] = self.config["GENERATE_ATOM"] - kw['feed_link_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"] - kw['currentfeed'] = None + kw['indexes_pretty_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"] + kw['show_index_page_navigation'] = self.config['SHOW_INDEX_PAGE_NAVIGATION'] + + if additional_dependencies is None: + additional_dependencies = [] # Split in smaller lists lists = [] @@ -2331,12 +2592,29 @@ class Nikola(object): while posts: lists.append(posts[:kw["index_display_post_count"]]) posts = posts[kw["index_display_post_count"]:] + if not lists: + lists.append([]) num_pages = len(lists) + displayed_page_numbers = [utils.get_displayed_page_number(i, num_pages, self) for i in range(num_pages)] + page_links = [page_link(i, page_number, num_pages, False) for i, page_number in enumerate(displayed_page_numbers)] + if kw['show_index_page_navigation']: + # Since the list displayed_page_numbers is not necessarily + # sorted -- in case INDEXES_STATIC is True, it is of the + # form [num_pages, 1, 2, ..., num_pages - 1] -- we order it + # via a map. This allows to not replicate the logic of + # utils.get_displayed_page_number() here. + if not kw["indexes_pages_main"] and not kw["indexes_static"]: + temp_map = {page_number: link for page_number, link in zip(displayed_page_numbers, page_links)} + else: + temp_map = {page_number - 1: link for page_number, link in zip(displayed_page_numbers, page_links)} + page_links_context = [temp_map[i] for i in range(num_pages)] 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 'has_other_languages' not in context: + context['has_other_languages'] = False + ipages_i = displayed_page_numbers[i] if kw["indexes_pages"]: indexes_pages = kw["indexes_pages"] % ipages_i else: @@ -2372,20 +2650,29 @@ class Nikola(object): if i < num_pages - 1: nextlink = i + 1 if prevlink is not None: - context["prevlink"] = page_link(prevlink, - utils.get_displayed_page_number(prevlink, num_pages, self), - num_pages, False) - context["prevfeedlink"] = page_link(prevlink, - utils.get_displayed_page_number(prevlink, num_pages, self), + context["prevlink"] = page_links[prevlink] + context["prevfeedlink"] = page_link(prevlink, displayed_page_numbers[prevlink], num_pages, False, extension=".atom") if nextlink is not None: - context["nextlink"] = page_link(nextlink, - utils.get_displayed_page_number(nextlink, num_pages, self), - num_pages, False) - context["nextfeedlink"] = page_link(nextlink, - utils.get_displayed_page_number(nextlink, num_pages, self), + context["nextlink"] = page_links[nextlink] + context["nextfeedlink"] = page_link(nextlink, displayed_page_numbers[nextlink], num_pages, False, extension=".atom") - context["permalink"] = page_link(i, ipages_i, num_pages, False) + context['show_index_page_navigation'] = kw['show_index_page_navigation'] + if kw['show_index_page_navigation']: + context['page_links'] = page_links_context + if not kw["indexes_pages_main"] and not kw["indexes_static"]: + context['current_page'] = ipages_i + else: + context['current_page'] = ipages_i - 1 + context['prev_next_links_reversed'] = kw['indexes_static'] + context["permalink"] = page_links[i] + context["is_frontmost_index"] = i == 0 + + # Add dependencies to featured posts + if 'featured' in context: + for post in context['featured']: + additional_dependencies += post.deps_uptodate(lang) + output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False)) task = self.generic_post_list_renderer( lang, @@ -2399,38 +2686,10 @@ class Nikola(object): task['basename'] = basename yield task - if kw['generate_atom']: - atom_output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False, extension=".atom")) - context["feedlink"] = page_link(i, ipages_i, num_pages, False, extension=".atom") - if not kw["currentfeed"]: - kw["currentfeed"] = context["feedlink"] - context["currentfeedlink"] = kw["currentfeed"] - context["feedpagenum"] = i - context["feedpagecount"] = num_pages - kw['feed_teasers'] = self.config['FEED_TEASERS'] - kw['feed_plain'] = self.config['FEED_PLAIN'] - kw['feed_previewimage'] = self.config['FEED_PREVIEWIMAGE'] - atom_task = { - "basename": basename, - "name": atom_output_name, - "file_dep": sorted([_.base_path for _ in post_list]), - "task_dep": ['render_posts'], - "targets": [atom_output_name], - "actions": [(self.atom_feed_renderer, - (lang, - post_list, - atom_output_name, - kw['filters'], - context,))], - "clean": True, - "uptodate": [utils.config_changed(kw, 'nikola.nikola.Nikola.atom_feed_renderer')] + additional_dependencies - } - yield utils.apply_filters(atom_task, kw['filters']) - - if kw["indexes_pages_main"] and kw['indexes_prety_page_url'](lang): + if kw["indexes_pages_main"] and kw['indexes_pretty_page_url'](lang): # create redirection - output_name = os.path.join(kw['output_folder'], page_path(0, utils.get_displayed_page_number(0, num_pages, self), num_pages, True)) - link = page_link(0, utils.get_displayed_page_number(0, num_pages, self), num_pages, False) + output_name = os.path.join(kw['output_folder'], page_path(0, displayed_page_numbers[0], num_pages, True)) + link = page_links[0] yield utils.apply_filters({ 'basename': basename, 'name': output_name, @@ -2440,159 +2699,58 @@ class Nikola(object): 'uptodate': [utils.config_changed(kw, 'nikola.nikola.Nikola.generic_index_renderer')], }, kw["filters"]) - def __repr__(self): - """Representation of a Nikola site.""" - return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE'](self.config['DEFAULT_LANG'])) - + def generic_atom_renderer(self, lang, posts, context_source, kw, basename, classification, kind, additional_dependencies=None): + """Create an Atom feed. -def sanitized_locales(locale_fallback, locale_default, locales, translations): - """Sanitize all locales availble in Nikola. - - There will be one locale for each language in translations. - - Locales for languages not in translations are ignored. + lang: The language + posts: A list of posts + context_source: This will be copied and extended and used as every + page's context + kw: An extended version will be used for uptodate dependencies + basename: Basename for task + classification: name of current classification (used to generate links) + kind: classification kind (used to generate links) + additional_dependencies: a list of dependencies which will be added + to task['uptodate'] + """ + # Update kw + kw = kw.copy() + kw["feed_length"] = self.config['FEED_LENGTH'] + kw['generate_atom'] = self.config["GENERATE_ATOM"] + kw['feed_links_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"] + kw['feed_teasers'] = self.config['FEED_TEASERS'] + kw['feed_plain'] = self.config['FEED_PLAIN'] - An explicit locale for a language can be specified in locales[language]. + if additional_dependencies is None: + additional_dependencies = [] - Locales at the input must be in the string style (like 'en', 'en.utf8'), and - the string can be unicode or bytes; at the output will be of type str, as - required by locale.setlocale. + post_list = posts[:kw["feed_length"]] + feedlink = self.link(kind + "_atom", classification, lang) + feedpath = self.path(kind + "_atom", classification, lang) - Explicit but invalid locales are replaced with the sanitized locale_fallback + context = context_source.copy() + if 'has_other_languages' not in context: + context['has_other_languages'] = False - Languages with no explicit locale are set to - the sanitized locale_default if it was explicitly set - sanitized guesses compatible with v 6.0.4 if locale_default was None + output_name = os.path.join(kw['output_folder'], feedpath) + context["feedlink"] = feedlink + task = { + "basename": basename, + "name": output_name, + "file_dep": sorted([_.base_path for _ in post_list]), + "task_dep": ['render_posts'], + "targets": [output_name], + "actions": [(self.atom_feed_renderer, + (lang, + post_list, + output_name, + kw['filters'], + context,))], + "clean": True, + "uptodate": [utils.config_changed(kw, 'nikola.nikola.Nikola.atom_feed_renderer')] + additional_dependencies + } + yield utils.apply_filters(task, kw['filters']) - NOTE: never use locale.getlocale() , it can return values that - locale.setlocale will not accept in Windows XP, 7 and pythons 2.6, 2.7, 3.3 - Examples: "Spanish", "French" can't do the full circle set / get / set - """ - if sys.platform != 'win32': - workaround_empty_LC_ALL_posix() - - # locales for languages not in translations are ignored - extras = set(locales) - set(translations) - if extras: - msg = 'Unexpected languages in LOCALES, ignoring them: {0}' - utils.LOGGER.warn(msg.format(', '.join(extras))) - for lang in extras: - del locales[lang] - - # py2x: get/setlocale related functions require the locale string as a str - # so convert - locale_fallback = str(locale_fallback) if locale_fallback else None - locale_default = str(locale_default) if locale_default else None - for lang in locales: - locales[lang] = str(locales[lang]) - - locale_fallback = valid_locale_fallback(locale_fallback) - - # explicit but invalid locales are replaced with the sanitized locale_fallback - for lang in locales: - if not is_valid_locale(locales[lang]): - msg = 'Locale {0} for language {1} not accepted by python locale.' - utils.LOGGER.warn(msg.format(locales[lang], lang)) - locales[lang] = locale_fallback - - # languages with no explicit locale - missing = set(translations) - set(locales) - if locale_default: - # are set to the sanitized locale_default if it was explicitly set - if not is_valid_locale(locale_default): - msg = 'LOCALE_DEFAULT {0} could not be set, using {1}' - utils.LOGGER.warn(msg.format(locale_default, locale_fallback)) - locale_default = locale_fallback - for lang in missing: - locales[lang] = locale_default - else: - # are set to sanitized guesses compatible with v 6.0.4 in Linux-Mac (was broken in Windows) - if sys.platform == 'win32': - guess_locale_fom_lang = guess_locale_from_lang_windows - else: - guess_locale_fom_lang = guess_locale_from_lang_posix - for lang in missing: - locale_n = guess_locale_fom_lang(lang) - if not locale_n: - locale_n = locale_fallback - msg = "Could not guess locale for language {0}, using locale {1}" - utils.LOGGER.warn(msg.format(lang, locale_n)) - utils.LOGGER.warn("Please fix your OS locale configuration or use the LOCALES option in conf.py to specify your preferred locale.") - if sys.platform != 'win32': - utils.LOGGER.warn("Make sure to use an UTF-8 locale to ensure Unicode support.") - locales[lang] = locale_n - - return locale_fallback, locale_default, locales - - -def is_valid_locale(locale_n): - """Check if locale (type str) is valid.""" - try: - locale.setlocale(locale.LC_ALL, locale_n) - return True - except locale.Error: - return False - - -def valid_locale_fallback(desired_locale=None): - """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 - """ - # Whenever fallbacks change, adjust test TestHarcodedFallbacksWork - candidates_windows = [str('English'), str('C')] - candidates_posix = [str('en_US.UTF-8'), str('C')] - candidates = candidates_windows if sys.platform == 'win32' else candidates_posix - if desired_locale: - candidates = list(candidates) - candidates.insert(0, desired_locale) - found_valid = False - for locale_n in candidates: - found_valid = is_valid_locale(locale_n) - if found_valid: - break - if not found_valid: - msg = 'Could not find a valid fallback locale, tried: {0}' - utils.LOGGER.warn(msg.format(candidates)) - elif desired_locale and (desired_locale != locale_n): - msg = 'Desired fallback locale {0} could not be set, using: {1}' - utils.LOGGER.warn(msg.format(desired_locale, locale_n)) - return locale_n - - -def guess_locale_from_lang_windows(lang): - """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) - else: - # this works in Travis when locale support set by Travis suggestion - locale_n = str((locale.normalize(lang).split('.')[0]) + '.UTF-8') - if not is_valid_locale(locale_n): - # http://thread.gmane.org/gmane.comp.web.nikola/337/focus=343 - locale_n = str((locale.normalize(lang).split('.')[0])) - if not is_valid_locale(locale_n): - locale_n = None - return locale_n - - -def workaround_empty_LC_ALL_posix(): - # clunky hack: we have seen some posix locales with all or most of LC_* - # defined to the same value, but with LC_ALL empty. - # Manually doing what we do here seems to work for nikola in that case. - # It is unknown if it will work when the LC_* aren't homogeneous - try: - lc_time = os.environ.get('LC_TIME', None) - lc_all = os.environ.get('LC_ALL', None) - if lc_time and not lc_all: - os.environ['LC_ALL'] = lc_time - except Exception: - pass + def __repr__(self): + """Representation of a Nikola site.""" + return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE'](self.config['DEFAULT_LANG'])) diff --git a/nikola/packages/README.md b/nikola/packages/README.md index ad94b00..7265069 100644 --- a/nikola/packages/README.md +++ b/nikola/packages/README.md @@ -2,6 +2,9 @@ We ship some third-party things with Nikola. They live here, along with their l Packages: - * tzlocal by Lennart Regebro, CC0 license (modified) + * tzlocal by Lennart Regebro, CC0 license (modified to remove pytz dependency) * datecond by Chris Warrick (Nikola contributor), 3-clause BSD license (modified) + * pygments_better_html by Chris Warrick (Nikola contributor), 3-clause BSD license, + portions copyright the Pygments team (2-clause BSD license). + diff --git a/nikola/packages/datecond/LICENSE b/nikola/packages/datecond/LICENSE index 5e8b6d6..d9980a8 100644 --- a/nikola/packages/datecond/LICENSE +++ b/nikola/packages/datecond/LICENSE @@ -1,4 +1,4 @@ -Copyright © 2016, Chris Warrick. +Copyright © 2016-2020, Chris Warrick. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/nikola/packages/datecond/__init__.py b/nikola/packages/datecond/__init__.py index b409057..92e7908 100644 --- a/nikola/packages/datecond/__init__.py +++ b/nikola/packages/datecond/__init__.py @@ -1,8 +1,7 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- -# Date Conditionals (datecond) -# Version 0.1.2 -# Copyright © 2015-2016, Chris Warrick. +# Date Conditionals v0.1.7 +# Copyright © 2015-2020, Chris Warrick. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -35,7 +34,7 @@ """Date range parser.""" -from __future__ import print_function, unicode_literals +import datetime import dateutil.parser import re import operator @@ -54,7 +53,7 @@ OPERATORS = { } -def date_in_range(date_range, date, debug=True): +def date_in_range(date_range, date, debug=False, now=None): """Check if date is in the range specified. Format: @@ -63,7 +62,10 @@ def date_in_range(date_range, date, debug=True): * attribute: year, month, day, hour, month, second, weekday, isoweekday or empty for full datetime * comparison_operator: == != <= >= < > - * value: integer or dateutil-compatible date input + * value: integer, 'now', 'today', or dateutil-compatible date input + + The optional `now` parameter can be used to provide a specific `now`/`today` value + (if none is provided, datetime.datetime.now()/datetime.date.today() is used). """ out = True @@ -73,6 +75,15 @@ def date_in_range(date_range, date, debug=True): if attribute in ('weekday', 'isoweekday'): left = getattr(date, attribute)() right = int(value) + elif value == 'now': + left = date + right = now or datetime.datetime.now() + elif value == 'today': + left = date.date() if isinstance(date, datetime.datetime) else date + if now: + right = now.date() if isinstance(now, datetime.datetime) else now + else: + right = datetime.date.today() elif attribute: left = getattr(date, attribute) right = int(value) diff --git a/nikola/packages/pygments_better_html/LICENSE b/nikola/packages/pygments_better_html/LICENSE new file mode 100644 index 0000000..196413e --- /dev/null +++ b/nikola/packages/pygments_better_html/LICENSE @@ -0,0 +1,30 @@ +Copyright © 2020, Chris Warrick. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions, and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author of this software nor the names of + contributors to this software may be used to endorse or promote + products derived from this software without specific prior written + consent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/nikola/packages/pygments_better_html/LICENSE.pygments b/nikola/packages/pygments_better_html/LICENSE.pygments new file mode 100644 index 0000000..13d1c74 --- /dev/null +++ b/nikola/packages/pygments_better_html/LICENSE.pygments @@ -0,0 +1,25 @@ +Copyright (c) 2006-2019 by the respective authors (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/nikola/packages/pygments_better_html/__init__.py b/nikola/packages/pygments_better_html/__init__.py new file mode 100644 index 0000000..ed6e004 --- /dev/null +++ b/nikola/packages/pygments_better_html/__init__.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +"""Better HTML formatter for Pygments. + +Copyright © 2020, Chris Warrick. +License: 3-clause BSD. +Portions copyright © 2006-2019, the Pygments authors. (2-clause BSD). +""" + +__all__ = ["BetterHtmlFormatter"] +__version__ = "0.1.4" + +import enum +import re +import warnings + +from pygments.formatters.html import HtmlFormatter + +MANY_SPACES = re.compile("( +)") + + +def _sp_to_nbsp(m): + return " " * (m.end() - m.start()) + + +class BetterLinenos(enum.Enum): + TABLE = "table" + OL = "ol" + + +class BetterHtmlFormatter(HtmlFormatter): + r""" + Format tokens as HTML 4 ``<span>`` tags, with alternate formatting styles. + + * ``linenos = 'table'`` renders each line of code in a separate table row + * ``linenos = 'ol'`` renders each line in a <li> element (inside <ol>) + + Both options allow word wrap and don't include line numbers when copying. + """ + + name = "HTML" + aliases = ["html"] + filenames = ["*.html", "*.htm"] + + def __init__(self, **options): + """Initialize the formatter.""" + super().__init__(**options) + self.linenos_name = self.options.get("linenos", "table") + if self.linenos_name is False: + self.linenos_val = False + self.linenos = 0 + elif self.linenos_name is True: + self.linenos_name = "table" + if self.linenos_name is not False: + self.linenos_val = BetterLinenos(self.linenos_name) + self.linenos = 2 if self.linenos_val == BetterLinenos.OL else 1 + + def get_style_defs(self, arg=None, wrapper_classes=None): + """Generate CSS style definitions. + + Return CSS style definitions for the classes produced by the current + highlighting style. ``arg`` can be a string or list of selectors to + insert before the token type classes. ``wrapper_classes`` are a list of + classes for the wrappers, defaults to the ``cssclass`` option. + """ + base = super().get_style_defs(arg) + new_styles = ( + ("{0} table, {0} tr, {0} td", "border-spacing: 0; border-collapse: separate; padding: 0"), + ("{0} pre", "white-space: pre-wrap; line-height: normal"), + ( + "{0}table td.linenos", + "vertical-align: top; padding-left: 10px; padding-right: 10px; user-select: none; -webkit-user-select: none", + ), + # Hack for Safari (user-select does not affect copy-paste) + ("{0}table td.linenos code:before", "content: attr(data-line-number)"), + ("{0}table td.code", "overflow-wrap: normal; border-collapse: collapse"), + ( + "{0}table td.code code", + "overflow: unset; border: none; padding: 0; margin: 0; white-space: pre-wrap; line-height: unset; background: none", + ), + ("{0} .lineno.nonumber", "list-style: none"), + ) + new_styles_code = [] + if wrapper_classes is None: + wrapper_classes = ["." + self.cssclass] + for cls, rule in new_styles: + new_styles_code.append(", ".join(cls.format(c) for c in wrapper_classes) + " { " + rule + " }") + return base + "\n" + "\n".join(new_styles_code) + + def _wrap_tablelinenos(self, inner): + lncount = 0 + codelines = [] + for t, line in inner: + if t: + lncount += 1 + codelines.append(line) + + fl = self.linenostart + mw = len(str(lncount + fl - 1)) + sp = self.linenospecial + st = self.linenostep + la = self.lineanchors + aln = self.anchorlinenos + nocls = self.noclasses + if sp: + lines = [] + + for i in range(fl, fl + lncount): + line_before = "" + line_after = "" + if i % st == 0: + if i % sp == 0: + if aln: + line_before = '<a href="#%s-%d" class="special">' % (la, i) + line_after = "</a>" + else: + line_before = '<span class="special">' + line_after = "</span>" + elif aln: + line_before = '<a href="#%s-%d">' % (la, i) + line_after = "</a>" + lines.append((line_before, "%*d" % (mw, i), line_after)) + else: + lines.append(("", "", "")) + else: + lines = [] + for i in range(fl, fl + lncount): + line_before = "" + line_after = "" + if i % st == 0: + if aln: + line_before = '<a href="#%s-%d">' % (la, i) + line_after = "</a>" + lines.append((line_before, "%*d" % (mw, i), line_after)) + else: + lines.append(("", "", "")) + + yield 0, '<div class="%s"><table class="%stable">' % ( + self.cssclass, + self.cssclass, + ) + for lndata, cl in zip(lines, codelines): + ln_b, ln, ln_a = lndata + cl = MANY_SPACES.sub(_sp_to_nbsp, cl) + if nocls: + yield 0, ( + '<tr><td class="linenos linenodiv" style="background-color: #f0f0f0; padding-right: 10px">' + ln_b + + '<code data-line-number="' + ln + '"></code>' + ln_a + '</td><td class="code"><code>' + cl + "</code></td></tr>" + ) + else: + yield 0, ( + '<tr><td class="linenos linenodiv">' + ln_b + '<code data-line-number="' + ln + + '"></code>' + ln_a + '</td><td class="code"><code>' + cl + "</code></td></tr>" + ) + yield 0, "</table></div>" + + def _wrap_inlinelinenos(self, inner): + # Override with new method + return self._wrap_ollineos(self, inner) + + def _wrap_ollinenos(self, inner): + lines = inner + sp = self.linenospecial + st = self.linenostep or 1 + num = self.linenostart + + if self.anchorlinenos: + warnings.warn("anchorlinenos is not supported for linenos='ol'.") + + yield 0, "<ol>" + if self.noclasses: + if sp: + for t, line in lines: + if num % sp == 0: + style = "background-color: #ffffc0; padding: 0 5px 0 5px" + else: + style = "background-color: #f0f0f0; padding: 0 5px 0 5px" + if num % st != 0: + style += "; list-style: none" + yield 1, '<li style="%s" value="%s">' % (style, num,) + line + "</li>" + num += 1 + else: + for t, line in lines: + yield 1, ( + '<li style="background-color: #f0f0f0; padding: 0 5px 0 5px%s" value="%s">' + % (("; list-style: none" if num % st != 0 else ""), num) + line + "</li>" + ) + num += 1 + elif sp: + for t, line in lines: + yield 1, '<li class="lineno%s%s" value="%s">' % ( + " special" if num % sp == 0 else "", + " nonumber" if num % st != 0 else "", + num, + ) + line + "</li>" + num += 1 + else: + for t, line in lines: + yield 1, '<li class="lineno%s" value="%s">' % ( + "" if num % st != 0 else " nonumber", + num, + ) + line + "</li>" + num += 1 + + yield 0, "</ol>" + + def format_unencoded(self, tokensource, outfile): + """Format code and write to outfile. + + The formatting process uses several nested generators; which of + them are used is determined by the user's options. + + Each generator should take at least one argument, ``inner``, + and wrap the pieces of text generated by this. + + Always yield 2-tuples: (code, text). If "code" is 1, the text + is part of the original tokensource being highlighted, if it's + 0, the text is some piece of wrapping. This makes it possible to + use several different wrappers that process the original source + linewise, e.g. line number generators. + """ + if self.linenos_val is False: + return super().format_unencoded(tokensource, outfile) + source = self._format_lines(tokensource) + if self.hl_lines: + source = self._highlight_lines(source) + if not self.nowrap: + if self.linenos_val == BetterLinenos.OL: + source = self._wrap_ollinenos(source) + if self.lineanchors: + source = self._wrap_lineanchors(source) + if self.linespans: + source = self._wrap_linespans(source) + if self.linenos_val == BetterLinenos.TABLE: + source = self._wrap_tablelinenos(source) + if self.linenos_val == BetterLinenos.OL: + source = self.wrap(source, outfile) + if self.full: + source = self._wrap_full(source, outfile) + + for t, piece in source: + outfile.write(piece) diff --git a/nikola/packages/tzlocal/__init__.py b/nikola/packages/tzlocal/__init__.py index 4a6b1d6..5b4947c 100644 --- a/nikola/packages/tzlocal/__init__.py +++ b/nikola/packages/tzlocal/__init__.py @@ -1,9 +1,8 @@ -"""tzlocal init.""" - +"""Try to figure out what your local timezone is.""" import sys -if sys.platform == 'win32': +__version__ = "2.0.0-nikola" + +if sys.platform == "win32": from .win32 import get_localzone, reload_localzone # NOQA -elif 'darwin' in sys.platform: - from .darwin import get_localzone, reload_localzone # NOQA else: from .unix import get_localzone, reload_localzone # NOQA diff --git a/nikola/packages/tzlocal/darwin.py b/nikola/packages/tzlocal/darwin.py deleted file mode 100644 index 0dbf1c1..0000000 --- a/nikola/packages/tzlocal/darwin.py +++ /dev/null @@ -1,43 +0,0 @@ -"""tzlocal for OS X.""" - -import os -import dateutil.tz -import subprocess - -_cache_tz = None - - -def _get_localzone(): - tzname = subprocess.check_output(["systemsetup", "-gettimezone"]).decode('utf-8') - tzname = tzname.replace("Time Zone: ", "") - # OS X 10.9+, this command is root-only - if 'exiting!' in tzname: - tzname = '' - - if not tzname: - # link will be something like /usr/share/zoneinfo/America/Los_Angeles. - link = os.readlink("/etc/localtime") - tzname = link.split('zoneinfo/')[-1] - tzname = tzname.strip() - try: - # test the name - assert tzname - dateutil.tz.gettz(tzname) - return tzname - except: - return None - - -def get_localzone(): - """Get the computers configured local timezone, if any.""" - global _cache_tz - if _cache_tz is None: - _cache_tz = _get_localzone() - return _cache_tz - - -def reload_localzone(): - """Reload the cached localzone. You need to call this if the timezone has changed.""" - global _cache_tz - _cache_tz = _get_localzone() - return _cache_tz diff --git a/nikola/packages/tzlocal/unix.py b/nikola/packages/tzlocal/unix.py index 8f7fc84..086ab7c 100644 --- a/nikola/packages/tzlocal/unix.py +++ b/nikola/packages/tzlocal/unix.py @@ -1,115 +1,128 @@ -"""tzlocal for UNIX.""" - -from __future__ import with_statement +"""Unix support for tzlocal.""" import os import re + import dateutil.tz _cache_tz = None -def _get_localzone(): - """Try to find the local timezone configuration. +def _try_tz_from_env(): + tzenv = os.environ.get("TZ") + if tzenv and tzenv[0] == ":": + tzenv = tzenv[1:] + try: + if tzenv: + dateutil.tz.gettz(tzenv) + return tzenv + except Exception: + pass + - This method prefers finding the timezone name and passing that to pytz, - over passing in the localtime file, as in the later case the zoneinfo - name is unknown. +def _get_localzone(_root="/"): + """Try to find the local timezone configuration. The parameter _root makes the function look for files like /etc/localtime beneath the _root directory. This is primarily used by the tests. In normal usage you call the function without parameters. """ - tz = os.environ.get('TZ') - if tz and tz[0] == ':': - tz = tz[1:] - try: - if tz: - dateutil.tz.gettz(tz) - return tz - except: - pass + tzenv = _try_tz_from_env() + if tzenv: + return tzenv - try: - # link will be something like /usr/share/zoneinfo/America/Los_Angeles. - link = os.readlink('/etc/localtime') - tz = link.split('zoneinfo/')[-1] + # Are we under Termux on Android? + if os.path.exists("/system/bin/getprop"): + import subprocess - if tz: - dateutil.tz.gettz(tz) - return tz - except: - return None + androidtz = ( + subprocess.check_output(["getprop", "persist.sys.timezone"]) + .strip() + .decode() + ) + return androidtz # Now look for distribution specific configuration files # that contain the timezone name. - tzpath = os.path.join('/etc/timezone') - if os.path.exists(tzpath): - with open(tzpath, 'rb') as tzfile: - data = tzfile.read() - - # Issue #3 was that /etc/timezone was a zoneinfo file. - # That's a misconfiguration, but we need to handle it gracefully: - if data[:5] != 'TZif2': + for configfile in ("etc/timezone", "var/db/zoneinfo"): + tzpath = os.path.join(_root, configfile) + try: + with open(tzpath, "rb") as tzfile: + data = tzfile.read() + + # Issue #3 was that /etc/timezone was a zoneinfo file. + # That's a misconfiguration, but we need to handle it gracefully: + if data[:5] == b"TZif2": + continue + etctz = data.strip().decode() - # Get rid of host definitions and comments: - if ' ' in etctz: - etctz, dummy = etctz.split(' ', 1) - if '#' in etctz: - etctz, dummy = etctz.split('#', 1) - tz = etctz.replace(' ', '_') - try: - if tz: - dateutil.tz.gettz(tz) - return tz - except: - pass + if not etctz: + # Empty file, skip + continue + for etctz in data.decode().splitlines(): + # Get rid of host definitions and comments: + if " " in etctz: + etctz, dummy = etctz.split(" ", 1) + if "#" in etctz: + etctz, dummy = etctz.split("#", 1) + if not etctz: + continue + tz = etctz.replace(" ", "_") + return tz + + except IOError: + # File doesn't exist or is a directory + continue # CentOS has a ZONE setting in /etc/sysconfig/clock, # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and # Gentoo has a TIMEZONE setting in /etc/conf.d/clock # We look through these files for a timezone: - zone_re = re.compile('\s*ZONE\s*=\s*\"') - timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"') - end_re = re.compile('\"') - - for tzpath in ('/etc/sysconfig/clock', '/etc/conf.d/clock'): - if not os.path.exists(tzpath): + zone_re = re.compile(r"\s*ZONE\s*=\s*\"") + timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"") + end_re = re.compile('"') + + for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): + tzpath = os.path.join(_root, filename) + try: + with open(tzpath, "rt") as tzfile: + data = tzfile.readlines() + + for line in data: + # Look for the ZONE= setting. + match = zone_re.match(line) + if match is None: + # No ZONE= setting. Look for the TIMEZONE= setting. + match = timezone_re.match(line) + if match is not None: + # Some setting existed + line = line[match.end():] + etctz = line[: end_re.search(line).start()] + + # We found a timezone + tz = etctz.replace(" ", "_") + return tz + + except IOError: + # File doesn't exist or is a directory continue - with open(tzpath, 'rt') as tzfile: - data = tzfile.readlines() - - for line in data: - # Look for the ZONE= setting. - match = zone_re.match(line) - if match is None: - # No ZONE= setting. Look for the TIMEZONE= setting. - match = timezone_re.match(line) - if match is not None: - # Some setting existed - line = line[match.end():] - etctz = line[:end_re.search(line).start()] - - # We found a timezone - tz = etctz.replace(' ', '_') - try: - if tz: - dateutil.tz.gettz(tz) - return tz - except: - pass - - # Nikola cannot use this thing below... - - # No explicit setting existed. Use localtime - # for filename in ('etc/localtime', 'usr/local/etc/localtime'): - # tzpath = os.path.join(_root, filename) - - # if not os.path.exists(tzpath): - # continue - # with open(tzpath, 'rb') as tzfile: - # return pytz.tzfile.build_tzinfo('local', tzfile) + # systemd distributions use symlinks that include the zone name, + # see manpage of localtime(5) and timedatectl(1) + tzpath = os.path.join(_root, "etc/localtime") + if os.path.exists(tzpath) and os.path.islink(tzpath): + tzpath = os.path.realpath(tzpath) + start = tzpath.find("/") + 1 + while start != 0: + tzpath = tzpath[start:] + try: + dateutil.tz.gettz(tzpath) + return tzpath + except Exception: + pass + start = tzpath.find("/") + 1 + + # Nothing found, return UTC return None @@ -118,6 +131,7 @@ def get_localzone(): global _cache_tz if _cache_tz is None: _cache_tz = _get_localzone() + return _cache_tz diff --git a/nikola/packages/tzlocal/win32.py b/nikola/packages/tzlocal/win32.py index cb19284..b8be8b4 100644 --- a/nikola/packages/tzlocal/win32.py +++ b/nikola/packages/tzlocal/win32.py @@ -1,12 +1,8 @@ -"""tzlocal for Windows.""" - +"""Windows support for tzlocal.""" try: import _winreg as winreg except ImportError: - try: - import winreg - except ImportError: - pass # not windows + import winreg from .windows_tz import win_tz @@ -24,7 +20,7 @@ def valuestodict(key): def get_localzone_name(): - """Get local time zone name.""" + """Get local 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 @@ -36,18 +32,19 @@ def get_localzone_name(): localtz = winreg.OpenKey(handle, TZLOCALKEYNAME) keyvalues = valuestodict(localtz) localtz.Close() - if 'TimeZoneKeyName' in keyvalues: + + if "TimeZoneKeyName" in keyvalues: # Windows 7 (and Vista?) # For some reason this returns a string with loads of NUL bytes at # least on some systems. I don't know if this is a bug somewhere, I # just work around it. - tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0] + tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0] else: # Windows 2000 or XP # This is the localized name: - tzwin = keyvalues['StandardName'] + tzwin = keyvalues["StandardName"] # Open the list of timezones to look up the real name: TZKEYNAME = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" @@ -60,15 +57,20 @@ def get_localzone_name(): sub = winreg.OpenKey(tzkey, subkey) data = valuestodict(sub) sub.Close() - if data['Std'] == tzwin: - tzkeyname = subkey - break + try: + if data["Std"] == tzwin: + tzkeyname = subkey + break + except KeyError: + # This timezone didn't have proper configuration. + # Ignore it. + pass tzkey.Close() handle.Close() if tzkeyname is None: - raise LookupError('Can not find Windows timezone configuration') + raise LookupError("Can not find Windows timezone configuration") timezone = win_tz.get(tzkeyname) if timezone is None: @@ -85,6 +87,7 @@ def get_localzone(): global _cache_tz if _cache_tz is None: _cache_tz = get_localzone_name() + return _cache_tz diff --git a/nikola/packages/tzlocal/windows_tz.py b/nikola/packages/tzlocal/windows_tz.py index c171aa5..0e911fd 100644 --- a/nikola/packages/tzlocal/windows_tz.py +++ b/nikola/packages/tzlocal/windows_tz.py @@ -1,544 +1,700 @@ -"""Windows timezone names.""" -# This file is autogenerated by the get_windows_info.py script +"""Windows timezone mapping.""" +# This file is autogenerated by the update_windows_mapping.py script # Do not edit. win_tz = { - 'AUS Central Standard Time': 'Australia/Darwin', - 'AUS Eastern Standard Time': 'Australia/Sydney', - 'Afghanistan Standard Time': 'Asia/Kabul', - 'Alaskan Standard Time': 'America/Anchorage', - 'Arab Standard Time': 'Asia/Riyadh', - 'Arabian Standard Time': 'Asia/Dubai', - 'Arabic Standard Time': 'Asia/Baghdad', - 'Argentina Standard Time': 'America/Buenos_Aires', - 'Atlantic Standard Time': 'America/Halifax', - 'Azerbaijan Standard Time': 'Asia/Baku', - 'Azores Standard Time': 'Atlantic/Azores', - 'Bahia Standard Time': 'America/Bahia', - 'Bangladesh Standard Time': 'Asia/Dhaka', - 'Canada Central Standard Time': 'America/Regina', - 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', - 'Caucasus Standard Time': 'Asia/Yerevan', - 'Cen. Australia Standard Time': 'Australia/Adelaide', - 'Central America Standard Time': 'America/Guatemala', - 'Central Asia Standard Time': 'Asia/Almaty', - 'Central Brazilian Standard Time': 'America/Cuiaba', - 'Central Europe Standard Time': 'Europe/Budapest', - 'Central European Standard Time': 'Europe/Warsaw', - 'Central Pacific Standard Time': 'Pacific/Guadalcanal', - 'Central Standard Time': 'America/Chicago', - 'Central Standard Time (Mexico)': 'America/Mexico_City', - 'China Standard Time': 'Asia/Shanghai', - 'Dateline Standard Time': 'Etc/GMT+12', - 'E. Africa Standard Time': 'Africa/Nairobi', - 'E. Australia Standard Time': 'Australia/Brisbane', - 'E. Europe Standard Time': 'Asia/Nicosia', - 'E. South America Standard Time': 'America/Sao_Paulo', - 'Eastern Standard Time': 'America/New_York', - 'Egypt Standard Time': 'Africa/Cairo', - 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', - 'FLE Standard Time': 'Europe/Kiev', - 'Fiji Standard Time': 'Pacific/Fiji', - 'GMT Standard Time': 'Europe/London', - 'GTB Standard Time': 'Europe/Bucharest', - 'Georgian Standard Time': 'Asia/Tbilisi', - 'Greenland Standard Time': 'America/Godthab', - 'Greenwich Standard Time': 'Atlantic/Reykjavik', - 'Hawaiian Standard Time': 'Pacific/Honolulu', - 'India Standard Time': 'Asia/Calcutta', - 'Iran Standard Time': 'Asia/Tehran', - 'Israel Standard Time': 'Asia/Jerusalem', - 'Jordan Standard Time': 'Asia/Amman', - 'Kaliningrad Standard Time': 'Europe/Kaliningrad', - 'Korea Standard Time': 'Asia/Seoul', - 'Libya Standard Time': 'Africa/Tripoli', - 'Magadan Standard Time': 'Asia/Magadan', - 'Mauritius Standard Time': 'Indian/Mauritius', - 'Middle East Standard Time': 'Asia/Beirut', - 'Montevideo Standard Time': 'America/Montevideo', - 'Morocco Standard Time': 'Africa/Casablanca', - 'Mountain Standard Time': 'America/Denver', - 'Mountain Standard Time (Mexico)': 'America/Chihuahua', - 'Myanmar Standard Time': 'Asia/Rangoon', - 'N. Central Asia Standard Time': 'Asia/Novosibirsk', - 'Namibia Standard Time': 'Africa/Windhoek', - 'Nepal Standard Time': 'Asia/Katmandu', - 'New Zealand Standard Time': 'Pacific/Auckland', - 'Newfoundland Standard Time': 'America/St_Johns', - 'North Asia East Standard Time': 'Asia/Irkutsk', - 'North Asia Standard Time': 'Asia/Krasnoyarsk', - 'Pacific SA Standard Time': 'America/Santiago', - 'Pacific Standard Time': 'America/Los_Angeles', - 'Pacific Standard Time (Mexico)': 'America/Santa_Isabel', - 'Pakistan Standard Time': 'Asia/Karachi', - 'Paraguay Standard Time': 'America/Asuncion', - 'Romance Standard Time': 'Europe/Paris', - 'Russian Standard Time': 'Europe/Moscow', - 'SA Eastern Standard Time': 'America/Cayenne', - 'SA Pacific Standard Time': 'America/Bogota', - 'SA Western Standard Time': 'America/La_Paz', - 'SE Asia Standard Time': 'Asia/Bangkok', - 'Samoa Standard Time': 'Pacific/Apia', - 'Singapore Standard Time': 'Asia/Singapore', - 'South Africa Standard Time': 'Africa/Johannesburg', - 'Sri Lanka Standard Time': 'Asia/Colombo', - 'Syria Standard Time': 'Asia/Damascus', - 'Taipei Standard Time': 'Asia/Taipei', - 'Tasmania Standard Time': 'Australia/Hobart', - 'Tokyo Standard Time': 'Asia/Tokyo', - 'Tonga Standard Time': 'Pacific/Tongatapu', - 'Turkey Standard Time': 'Europe/Istanbul', - 'US Eastern Standard Time': 'America/Indianapolis', - 'US Mountain Standard Time': 'America/Phoenix', - 'UTC': 'Etc/GMT', - 'UTC+12': 'Etc/GMT-12', - 'UTC-02': 'Etc/GMT+2', - 'UTC-11': 'Etc/GMT+11', - 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', - 'Venezuela Standard Time': 'America/Caracas', - 'Vladivostok Standard Time': 'Asia/Vladivostok', - 'W. Australia Standard Time': 'Australia/Perth', - 'W. Central Africa Standard Time': 'Africa/Lagos', - 'W. Europe Standard Time': 'Europe/Berlin', - 'West Asia Standard Time': 'Asia/Tashkent', - 'West Pacific Standard Time': 'Pacific/Port_Moresby', - 'Yakutsk Standard Time': 'Asia/Yakutsk' + "AUS Central Standard Time": "Australia/Darwin", + "AUS Eastern Standard Time": "Australia/Sydney", + "Afghanistan Standard Time": "Asia/Kabul", + "Alaskan Standard Time": "America/Anchorage", + "Aleutian Standard Time": "America/Adak", + "Altai Standard Time": "Asia/Barnaul", + "Arab Standard Time": "Asia/Riyadh", + "Arabian Standard Time": "Asia/Dubai", + "Arabic Standard Time": "Asia/Baghdad", + "Argentina Standard Time": "America/Buenos_Aires", + "Astrakhan Standard Time": "Europe/Astrakhan", + "Atlantic Standard Time": "America/Halifax", + "Aus Central W. Standard Time": "Australia/Eucla", + "Azerbaijan Standard Time": "Asia/Baku", + "Azores Standard Time": "Atlantic/Azores", + "Bahia Standard Time": "America/Bahia", + "Bangladesh Standard Time": "Asia/Dhaka", + "Belarus Standard Time": "Europe/Minsk", + "Bougainville Standard Time": "Pacific/Bougainville", + "Canada Central Standard Time": "America/Regina", + "Cape Verde Standard Time": "Atlantic/Cape_Verde", + "Caucasus Standard Time": "Asia/Yerevan", + "Cen. Australia Standard Time": "Australia/Adelaide", + "Central America Standard Time": "America/Guatemala", + "Central Asia Standard Time": "Asia/Almaty", + "Central Brazilian Standard Time": "America/Cuiaba", + "Central Europe Standard Time": "Europe/Budapest", + "Central European Standard Time": "Europe/Warsaw", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Central Standard Time": "America/Chicago", + "Central Standard Time (Mexico)": "America/Mexico_City", + "Chatham Islands Standard Time": "Pacific/Chatham", + "China Standard Time": "Asia/Shanghai", + "Cuba Standard Time": "America/Havana", + "Dateline Standard Time": "Etc/GMT+12", + "E. Africa Standard Time": "Africa/Nairobi", + "E. Australia Standard Time": "Australia/Brisbane", + "E. Europe Standard Time": "Europe/Chisinau", + "E. South America Standard Time": "America/Sao_Paulo", + "Easter Island Standard Time": "Pacific/Easter", + "Eastern Standard Time": "America/New_York", + "Eastern Standard Time (Mexico)": "America/Cancun", + "Egypt Standard Time": "Africa/Cairo", + "Ekaterinburg Standard Time": "Asia/Yekaterinburg", + "FLE Standard Time": "Europe/Kiev", + "Fiji Standard Time": "Pacific/Fiji", + "GMT Standard Time": "Europe/London", + "GTB Standard Time": "Europe/Bucharest", + "Georgian Standard Time": "Asia/Tbilisi", + "Greenland Standard Time": "America/Godthab", + "Greenwich Standard Time": "Atlantic/Reykjavik", + "Haiti Standard Time": "America/Port-au-Prince", + "Hawaiian Standard Time": "Pacific/Honolulu", + "India Standard Time": "Asia/Calcutta", + "Iran Standard Time": "Asia/Tehran", + "Israel Standard Time": "Asia/Jerusalem", + "Jordan Standard Time": "Asia/Amman", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "Korea Standard Time": "Asia/Seoul", + "Libya Standard Time": "Africa/Tripoli", + "Line Islands Standard Time": "Pacific/Kiritimati", + "Lord Howe Standard Time": "Australia/Lord_Howe", + "Magadan Standard Time": "Asia/Magadan", + "Magallanes Standard Time": "America/Punta_Arenas", + "Marquesas Standard Time": "Pacific/Marquesas", + "Mauritius Standard Time": "Indian/Mauritius", + "Middle East Standard Time": "Asia/Beirut", + "Montevideo Standard Time": "America/Montevideo", + "Morocco Standard Time": "Africa/Casablanca", + "Mountain Standard Time": "America/Denver", + "Mountain Standard Time (Mexico)": "America/Chihuahua", + "Myanmar Standard Time": "Asia/Rangoon", + "N. Central Asia Standard Time": "Asia/Novosibirsk", + "Namibia Standard Time": "Africa/Windhoek", + "Nepal Standard Time": "Asia/Katmandu", + "New Zealand Standard Time": "Pacific/Auckland", + "Newfoundland Standard Time": "America/St_Johns", + "Norfolk Standard Time": "Pacific/Norfolk", + "North Asia East Standard Time": "Asia/Irkutsk", + "North Asia Standard Time": "Asia/Krasnoyarsk", + "North Korea Standard Time": "Asia/Pyongyang", + "Omsk Standard Time": "Asia/Omsk", + "Pacific SA Standard Time": "America/Santiago", + "Pacific Standard Time": "America/Los_Angeles", + "Pacific Standard Time (Mexico)": "America/Tijuana", + "Pakistan Standard Time": "Asia/Karachi", + "Paraguay Standard Time": "America/Asuncion", + "Romance Standard Time": "Europe/Paris", + "Russia Time Zone 10": "Asia/Srednekolymsk", + "Russia Time Zone 11": "Asia/Kamchatka", + "Russia Time Zone 3": "Europe/Samara", + "Russian Standard Time": "Europe/Moscow", + "SA Eastern Standard Time": "America/Cayenne", + "SA Pacific Standard Time": "America/Bogota", + "SA Western Standard Time": "America/La_Paz", + "SE Asia Standard Time": "Asia/Bangkok", + "Saint Pierre Standard Time": "America/Miquelon", + "Sakhalin Standard Time": "Asia/Sakhalin", + "Samoa Standard Time": "Pacific/Apia", + "Sao Tome Standard Time": "Africa/Sao_Tome", + "Saratov Standard Time": "Europe/Saratov", + "Singapore Standard Time": "Asia/Singapore", + "South Africa Standard Time": "Africa/Johannesburg", + "Sri Lanka Standard Time": "Asia/Colombo", + "Sudan Standard Time": "Africa/Khartoum", + "Syria Standard Time": "Asia/Damascus", + "Taipei Standard Time": "Asia/Taipei", + "Tasmania Standard Time": "Australia/Hobart", + "Tocantins Standard Time": "America/Araguaina", + "Tokyo Standard Time": "Asia/Tokyo", + "Tomsk Standard Time": "Asia/Tomsk", + "Tonga Standard Time": "Pacific/Tongatapu", + "Transbaikal Standard Time": "Asia/Chita", + "Turkey Standard Time": "Europe/Istanbul", + "Turks And Caicos Standard Time": "America/Grand_Turk", + "US Eastern Standard Time": "America/Indianapolis", + "US Mountain Standard Time": "America/Phoenix", + "UTC": "Etc/GMT", + "UTC+12": "Etc/GMT-12", + "UTC+13": "Etc/GMT-13", + "UTC-02": "Etc/GMT+2", + "UTC-08": "Etc/GMT+8", + "UTC-09": "Etc/GMT+9", + "UTC-11": "Etc/GMT+11", + "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", + "Venezuela Standard Time": "America/Caracas", + "Vladivostok Standard Time": "Asia/Vladivostok", + "W. Australia Standard Time": "Australia/Perth", + "W. Central Africa Standard Time": "Africa/Lagos", + "W. Europe Standard Time": "Europe/Berlin", + "W. Mongolia Standard Time": "Asia/Hovd", + "West Asia Standard Time": "Asia/Tashkent", + "West Bank Standard Time": "Asia/Hebron", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Yakutsk Standard Time": "Asia/Yakutsk", } # Old name for the win_tz variable: tz_names = win_tz tz_win = { - 'Africa/Abidjan': 'Greenwich Standard Time', - 'Africa/Accra': 'Greenwich Standard Time', - 'Africa/Addis_Ababa': 'E. Africa Standard Time', - 'Africa/Algiers': 'W. Central Africa Standard Time', - 'Africa/Asmera': 'E. Africa Standard Time', - 'Africa/Bamako': 'Greenwich Standard Time', - 'Africa/Bangui': 'W. Central Africa Standard Time', - 'Africa/Banjul': 'Greenwich Standard Time', - 'Africa/Bissau': 'Greenwich Standard Time', - 'Africa/Blantyre': 'South Africa Standard Time', - 'Africa/Brazzaville': 'W. Central Africa Standard Time', - 'Africa/Bujumbura': 'South Africa Standard Time', - 'Africa/Cairo': 'Egypt Standard Time', - 'Africa/Casablanca': 'Morocco Standard Time', - 'Africa/Ceuta': 'Romance Standard Time', - 'Africa/Conakry': 'Greenwich Standard Time', - 'Africa/Dakar': 'Greenwich Standard Time', - 'Africa/Dar_es_Salaam': 'E. Africa Standard Time', - 'Africa/Djibouti': 'E. Africa Standard Time', - 'Africa/Douala': 'W. Central Africa Standard Time', - 'Africa/El_Aaiun': 'Morocco Standard Time', - 'Africa/Freetown': 'Greenwich Standard Time', - 'Africa/Gaborone': 'South Africa Standard Time', - 'Africa/Harare': 'South Africa Standard Time', - 'Africa/Johannesburg': 'South Africa Standard Time', - 'Africa/Juba': 'E. Africa Standard Time', - 'Africa/Kampala': 'E. Africa Standard Time', - 'Africa/Khartoum': 'E. Africa Standard Time', - 'Africa/Kigali': 'South Africa Standard Time', - 'Africa/Kinshasa': 'W. Central Africa Standard Time', - 'Africa/Lagos': 'W. Central Africa Standard Time', - 'Africa/Libreville': 'W. Central Africa Standard Time', - 'Africa/Lome': 'Greenwich Standard Time', - 'Africa/Luanda': 'W. Central Africa Standard Time', - 'Africa/Lubumbashi': 'South Africa Standard Time', - 'Africa/Lusaka': 'South Africa Standard Time', - 'Africa/Malabo': 'W. Central Africa Standard Time', - 'Africa/Maputo': 'South Africa Standard Time', - 'Africa/Maseru': 'South Africa Standard Time', - 'Africa/Mbabane': 'South Africa Standard Time', - 'Africa/Mogadishu': 'E. Africa Standard Time', - 'Africa/Monrovia': 'Greenwich Standard Time', - 'Africa/Nairobi': 'E. Africa Standard Time', - 'Africa/Ndjamena': 'W. Central Africa Standard Time', - 'Africa/Niamey': 'W. Central Africa Standard Time', - 'Africa/Nouakchott': 'Greenwich Standard Time', - 'Africa/Ouagadougou': 'Greenwich Standard Time', - 'Africa/Porto-Novo': 'W. Central Africa Standard Time', - 'Africa/Sao_Tome': 'Greenwich Standard Time', - 'Africa/Tripoli': 'Libya Standard Time', - 'Africa/Tunis': 'W. Central Africa Standard Time', - 'Africa/Windhoek': 'Namibia Standard Time', - 'America/Anchorage': 'Alaskan Standard Time', - 'America/Anguilla': 'SA Western Standard Time', - 'America/Antigua': 'SA Western Standard Time', - 'America/Araguaina': 'SA Eastern Standard Time', - 'America/Argentina/La_Rioja': 'Argentina Standard Time', - 'America/Argentina/Rio_Gallegos': 'Argentina Standard Time', - 'America/Argentina/Salta': 'Argentina Standard Time', - 'America/Argentina/San_Juan': 'Argentina Standard Time', - 'America/Argentina/San_Luis': 'Argentina Standard Time', - 'America/Argentina/Tucuman': 'Argentina Standard Time', - 'America/Argentina/Ushuaia': 'Argentina Standard Time', - 'America/Aruba': 'SA Western Standard Time', - 'America/Asuncion': 'Paraguay Standard Time', - 'America/Bahia': 'Bahia Standard Time', - 'America/Bahia_Banderas': 'Central Standard Time (Mexico)', - 'America/Barbados': 'SA Western Standard Time', - 'America/Belem': 'SA Eastern Standard Time', - 'America/Belize': 'Central America Standard Time', - 'America/Blanc-Sablon': 'SA Western Standard Time', - 'America/Boa_Vista': 'SA Western Standard Time', - 'America/Bogota': 'SA Pacific Standard Time', - 'America/Boise': 'Mountain Standard Time', - 'America/Buenos_Aires': 'Argentina Standard Time', - 'America/Cambridge_Bay': 'Mountain Standard Time', - 'America/Campo_Grande': 'Central Brazilian Standard Time', - 'America/Cancun': 'Central Standard Time (Mexico)', - 'America/Caracas': 'Venezuela Standard Time', - 'America/Catamarca': 'Argentina Standard Time', - 'America/Cayenne': 'SA Eastern Standard Time', - 'America/Cayman': 'SA Pacific Standard Time', - 'America/Chicago': 'Central Standard Time', - 'America/Chihuahua': 'Mountain Standard Time (Mexico)', - 'America/Coral_Harbour': 'SA Pacific Standard Time', - 'America/Cordoba': 'Argentina Standard Time', - 'America/Costa_Rica': 'Central America Standard Time', - 'America/Creston': 'US Mountain Standard Time', - 'America/Cuiaba': 'Central Brazilian Standard Time', - 'America/Curacao': 'SA Western Standard Time', - 'America/Danmarkshavn': 'UTC', - 'America/Dawson': 'Pacific Standard Time', - 'America/Dawson_Creek': 'US Mountain Standard Time', - 'America/Denver': 'Mountain Standard Time', - 'America/Detroit': 'Eastern Standard Time', - 'America/Dominica': 'SA Western Standard Time', - 'America/Edmonton': 'Mountain Standard Time', - 'America/Eirunepe': 'SA Pacific Standard Time', - 'America/El_Salvador': 'Central America Standard Time', - 'America/Fortaleza': 'SA Eastern Standard Time', - 'America/Glace_Bay': 'Atlantic Standard Time', - 'America/Godthab': 'Greenland Standard Time', - 'America/Goose_Bay': 'Atlantic Standard Time', - 'America/Grand_Turk': 'Eastern Standard Time', - 'America/Grenada': 'SA Western Standard Time', - 'America/Guadeloupe': 'SA Western Standard Time', - 'America/Guatemala': 'Central America Standard Time', - 'America/Guayaquil': 'SA Pacific Standard Time', - 'America/Guyana': 'SA Western Standard Time', - 'America/Halifax': 'Atlantic Standard Time', - 'America/Havana': 'Eastern Standard Time', - 'America/Hermosillo': 'US Mountain Standard Time', - 'America/Indiana/Knox': 'Central Standard Time', - 'America/Indiana/Marengo': 'US Eastern Standard Time', - 'America/Indiana/Petersburg': 'Eastern Standard Time', - 'America/Indiana/Tell_City': 'Central Standard Time', - 'America/Indiana/Vevay': 'US Eastern Standard Time', - 'America/Indiana/Vincennes': 'Eastern Standard Time', - 'America/Indiana/Winamac': 'Eastern Standard Time', - 'America/Indianapolis': 'US Eastern Standard Time', - 'America/Inuvik': 'Mountain Standard Time', - 'America/Iqaluit': 'Eastern Standard Time', - 'America/Jamaica': 'SA Pacific Standard Time', - 'America/Jujuy': 'Argentina Standard Time', - 'America/Juneau': 'Alaskan Standard Time', - 'America/Kentucky/Monticello': 'Eastern Standard Time', - 'America/Kralendijk': 'SA Western Standard Time', - 'America/La_Paz': 'SA Western Standard Time', - 'America/Lima': 'SA Pacific Standard Time', - 'America/Los_Angeles': 'Pacific Standard Time', - 'America/Louisville': 'Eastern Standard Time', - 'America/Lower_Princes': 'SA Western Standard Time', - 'America/Maceio': 'SA Eastern Standard Time', - 'America/Managua': 'Central America Standard Time', - 'America/Manaus': 'SA Western Standard Time', - 'America/Marigot': 'SA Western Standard Time', - 'America/Martinique': 'SA Western Standard Time', - 'America/Matamoros': 'Central Standard Time', - 'America/Mazatlan': 'Mountain Standard Time (Mexico)', - 'America/Mendoza': 'Argentina Standard Time', - 'America/Menominee': 'Central Standard Time', - 'America/Merida': 'Central Standard Time (Mexico)', - 'America/Mexico_City': 'Central Standard Time (Mexico)', - 'America/Moncton': 'Atlantic Standard Time', - 'America/Monterrey': 'Central Standard Time (Mexico)', - 'America/Montevideo': 'Montevideo Standard Time', - 'America/Montreal': 'Eastern Standard Time', - 'America/Montserrat': 'SA Western Standard Time', - 'America/Nassau': 'Eastern Standard Time', - 'America/New_York': 'Eastern Standard Time', - 'America/Nipigon': 'Eastern Standard Time', - 'America/Nome': 'Alaskan Standard Time', - 'America/Noronha': 'UTC-02', - 'America/North_Dakota/Beulah': 'Central Standard Time', - 'America/North_Dakota/Center': 'Central Standard Time', - 'America/North_Dakota/New_Salem': 'Central Standard Time', - 'America/Ojinaga': 'Mountain Standard Time', - 'America/Panama': 'SA Pacific Standard Time', - 'America/Pangnirtung': 'Eastern Standard Time', - 'America/Paramaribo': 'SA Eastern Standard Time', - 'America/Phoenix': 'US Mountain Standard Time', - 'America/Port-au-Prince': 'Eastern Standard Time', - 'America/Port_of_Spain': 'SA Western Standard Time', - 'America/Porto_Velho': 'SA Western Standard Time', - 'America/Puerto_Rico': 'SA Western Standard Time', - 'America/Rainy_River': 'Central Standard Time', - 'America/Rankin_Inlet': 'Central Standard Time', - 'America/Recife': 'SA Eastern Standard Time', - 'America/Regina': 'Canada Central Standard Time', - 'America/Resolute': 'Central Standard Time', - 'America/Rio_Branco': 'SA Pacific Standard Time', - 'America/Santa_Isabel': 'Pacific Standard Time (Mexico)', - 'America/Santarem': 'SA Eastern Standard Time', - 'America/Santiago': 'Pacific SA Standard Time', - 'America/Santo_Domingo': 'SA Western Standard Time', - 'America/Sao_Paulo': 'E. South America Standard Time', - 'America/Scoresbysund': 'Azores Standard Time', - 'America/Shiprock': 'Mountain Standard Time', - 'America/Sitka': 'Alaskan Standard Time', - 'America/St_Barthelemy': 'SA Western Standard Time', - 'America/St_Johns': 'Newfoundland Standard Time', - 'America/St_Kitts': 'SA Western Standard Time', - 'America/St_Lucia': 'SA Western Standard Time', - 'America/St_Thomas': 'SA Western Standard Time', - 'America/St_Vincent': 'SA Western Standard Time', - 'America/Swift_Current': 'Canada Central Standard Time', - 'America/Tegucigalpa': 'Central America Standard Time', - 'America/Thule': 'Atlantic Standard Time', - 'America/Thunder_Bay': 'Eastern Standard Time', - 'America/Tijuana': 'Pacific Standard Time', - 'America/Toronto': 'Eastern Standard Time', - 'America/Tortola': 'SA Western Standard Time', - 'America/Vancouver': 'Pacific Standard Time', - 'America/Whitehorse': 'Pacific Standard Time', - 'America/Winnipeg': 'Central Standard Time', - 'America/Yakutat': 'Alaskan Standard Time', - 'America/Yellowknife': 'Mountain Standard Time', - 'Antarctica/Casey': 'W. Australia Standard Time', - 'Antarctica/Davis': 'SE Asia Standard Time', - 'Antarctica/DumontDUrville': 'West Pacific Standard Time', - 'Antarctica/Macquarie': 'Central Pacific Standard Time', - 'Antarctica/Mawson': 'West Asia Standard Time', - 'Antarctica/McMurdo': 'New Zealand Standard Time', - 'Antarctica/Palmer': 'Pacific SA Standard Time', - 'Antarctica/Rothera': 'SA Eastern Standard Time', - 'Antarctica/South_Pole': 'New Zealand Standard Time', - 'Antarctica/Syowa': 'E. Africa Standard Time', - 'Antarctica/Vostok': 'Central Asia Standard Time', - 'Arctic/Longyearbyen': 'W. Europe Standard Time', - 'Asia/Aden': 'Arab Standard Time', - 'Asia/Almaty': 'Central Asia Standard Time', - 'Asia/Amman': 'Jordan Standard Time', - 'Asia/Anadyr': 'Magadan Standard Time', - 'Asia/Aqtau': 'West Asia Standard Time', - 'Asia/Aqtobe': 'West Asia Standard Time', - 'Asia/Ashgabat': 'West Asia Standard Time', - 'Asia/Baghdad': 'Arabic Standard Time', - 'Asia/Bahrain': 'Arab Standard Time', - 'Asia/Baku': 'Azerbaijan Standard Time', - 'Asia/Bangkok': 'SE Asia Standard Time', - 'Asia/Beirut': 'Middle East Standard Time', - 'Asia/Bishkek': 'Central Asia Standard Time', - 'Asia/Brunei': 'Singapore Standard Time', - 'Asia/Calcutta': 'India Standard Time', - 'Asia/Choibalsan': 'Ulaanbaatar Standard Time', - 'Asia/Chongqing': 'China Standard Time', - 'Asia/Colombo': 'Sri Lanka Standard Time', - 'Asia/Damascus': 'Syria Standard Time', - 'Asia/Dhaka': 'Bangladesh Standard Time', - 'Asia/Dili': 'Tokyo Standard Time', - 'Asia/Dubai': 'Arabian Standard Time', - 'Asia/Dushanbe': 'West Asia Standard Time', - 'Asia/Harbin': 'China Standard Time', - 'Asia/Hong_Kong': 'China Standard Time', - 'Asia/Hovd': 'SE Asia Standard Time', - 'Asia/Irkutsk': 'North Asia East Standard Time', - 'Asia/Jakarta': 'SE Asia Standard Time', - 'Asia/Jayapura': 'Tokyo Standard Time', - 'Asia/Jerusalem': 'Israel Standard Time', - 'Asia/Kabul': 'Afghanistan Standard Time', - 'Asia/Kamchatka': 'Magadan Standard Time', - 'Asia/Karachi': 'Pakistan Standard Time', - 'Asia/Kashgar': 'China Standard Time', - 'Asia/Katmandu': 'Nepal Standard Time', - 'Asia/Khandyga': 'Yakutsk Standard Time', - 'Asia/Krasnoyarsk': 'North Asia Standard Time', - 'Asia/Kuala_Lumpur': 'Singapore Standard Time', - 'Asia/Kuching': 'Singapore Standard Time', - 'Asia/Kuwait': 'Arab Standard Time', - 'Asia/Macau': 'China Standard Time', - 'Asia/Magadan': 'Magadan Standard Time', - 'Asia/Makassar': 'Singapore Standard Time', - 'Asia/Manila': 'Singapore Standard Time', - 'Asia/Muscat': 'Arabian Standard Time', - 'Asia/Nicosia': 'E. Europe Standard Time', - 'Asia/Novokuznetsk': 'N. Central Asia Standard Time', - 'Asia/Novosibirsk': 'N. Central Asia Standard Time', - 'Asia/Omsk': 'N. Central Asia Standard Time', - 'Asia/Oral': 'West Asia Standard Time', - 'Asia/Phnom_Penh': 'SE Asia Standard Time', - 'Asia/Pontianak': 'SE Asia Standard Time', - 'Asia/Pyongyang': 'Korea Standard Time', - 'Asia/Qatar': 'Arab Standard Time', - 'Asia/Qyzylorda': 'Central Asia Standard Time', - 'Asia/Rangoon': 'Myanmar Standard Time', - 'Asia/Riyadh': 'Arab Standard Time', - 'Asia/Saigon': 'SE Asia Standard Time', - 'Asia/Sakhalin': 'Vladivostok Standard Time', - 'Asia/Samarkand': 'West Asia Standard Time', - 'Asia/Seoul': 'Korea Standard Time', - 'Asia/Shanghai': 'China Standard Time', - 'Asia/Singapore': 'Singapore Standard Time', - 'Asia/Taipei': 'Taipei Standard Time', - 'Asia/Tashkent': 'West Asia Standard Time', - 'Asia/Tbilisi': 'Georgian Standard Time', - 'Asia/Tehran': 'Iran Standard Time', - 'Asia/Thimphu': 'Bangladesh Standard Time', - 'Asia/Tokyo': 'Tokyo Standard Time', - 'Asia/Ulaanbaatar': 'Ulaanbaatar Standard Time', - 'Asia/Urumqi': 'China Standard Time', - 'Asia/Ust-Nera': 'Vladivostok Standard Time', - 'Asia/Vientiane': 'SE Asia Standard Time', - 'Asia/Vladivostok': 'Vladivostok Standard Time', - 'Asia/Yakutsk': 'Yakutsk Standard Time', - 'Asia/Yekaterinburg': 'Ekaterinburg Standard Time', - 'Asia/Yerevan': 'Caucasus Standard Time', - 'Atlantic/Azores': 'Azores Standard Time', - 'Atlantic/Bermuda': 'Atlantic Standard Time', - 'Atlantic/Canary': 'GMT Standard Time', - 'Atlantic/Cape_Verde': 'Cape Verde Standard Time', - 'Atlantic/Faeroe': 'GMT Standard Time', - 'Atlantic/Madeira': 'GMT Standard Time', - 'Atlantic/Reykjavik': 'Greenwich Standard Time', - 'Atlantic/South_Georgia': 'UTC-02', - 'Atlantic/St_Helena': 'Greenwich Standard Time', - 'Atlantic/Stanley': 'SA Eastern Standard Time', - 'Australia/Adelaide': 'Cen. Australia Standard Time', - 'Australia/Brisbane': 'E. Australia Standard Time', - 'Australia/Broken_Hill': 'Cen. Australia Standard Time', - 'Australia/Currie': 'Tasmania Standard Time', - 'Australia/Darwin': 'AUS Central Standard Time', - 'Australia/Hobart': 'Tasmania Standard Time', - 'Australia/Lindeman': 'E. Australia Standard Time', - 'Australia/Melbourne': 'AUS Eastern Standard Time', - 'Australia/Perth': 'W. Australia Standard Time', - 'Australia/Sydney': 'AUS Eastern Standard Time', - 'CST6CDT': 'Central Standard Time', - 'EST5EDT': 'Eastern Standard Time', - 'Etc/GMT': 'UTC', - 'Etc/GMT+1': 'Cape Verde Standard Time', - 'Etc/GMT+10': 'Hawaiian Standard Time', - 'Etc/GMT+11': 'UTC-11', - 'Etc/GMT+12': 'Dateline Standard Time', - 'Etc/GMT+2': 'UTC-02', - 'Etc/GMT+3': 'SA Eastern Standard Time', - 'Etc/GMT+4': 'SA Western Standard Time', - 'Etc/GMT+5': 'SA Pacific Standard Time', - 'Etc/GMT+6': 'Central America Standard Time', - 'Etc/GMT+7': 'US Mountain Standard Time', - 'Etc/GMT-1': 'W. Central Africa Standard Time', - 'Etc/GMT-10': 'West Pacific Standard Time', - 'Etc/GMT-11': 'Central Pacific Standard Time', - 'Etc/GMT-12': 'UTC+12', - 'Etc/GMT-13': 'Tonga Standard Time', - 'Etc/GMT-2': 'South Africa Standard Time', - 'Etc/GMT-3': 'E. Africa Standard Time', - 'Etc/GMT-4': 'Arabian Standard Time', - 'Etc/GMT-5': 'West Asia Standard Time', - 'Etc/GMT-6': 'Central Asia Standard Time', - 'Etc/GMT-7': 'SE Asia Standard Time', - 'Etc/GMT-8': 'Singapore Standard Time', - 'Etc/GMT-9': 'Tokyo Standard Time', - 'Etc/UTC': 'UTC', - 'Europe/Amsterdam': 'W. Europe Standard Time', - 'Europe/Andorra': 'W. Europe Standard Time', - 'Europe/Athens': 'GTB Standard Time', - 'Europe/Belgrade': 'Central Europe Standard Time', - 'Europe/Berlin': 'W. Europe Standard Time', - 'Europe/Bratislava': 'Central Europe Standard Time', - 'Europe/Brussels': 'Romance Standard Time', - 'Europe/Bucharest': 'GTB Standard Time', - 'Europe/Budapest': 'Central Europe Standard Time', - 'Europe/Busingen': 'W. Europe Standard Time', - 'Europe/Chisinau': 'GTB Standard Time', - 'Europe/Copenhagen': 'Romance Standard Time', - 'Europe/Dublin': 'GMT Standard Time', - 'Europe/Gibraltar': 'W. Europe Standard Time', - 'Europe/Guernsey': 'GMT Standard Time', - 'Europe/Helsinki': 'FLE Standard Time', - 'Europe/Isle_of_Man': 'GMT Standard Time', - 'Europe/Istanbul': 'Turkey Standard Time', - 'Europe/Jersey': 'GMT Standard Time', - 'Europe/Kaliningrad': 'Kaliningrad Standard Time', - 'Europe/Kiev': 'FLE Standard Time', - 'Europe/Lisbon': 'GMT Standard Time', - 'Europe/Ljubljana': 'Central Europe Standard Time', - 'Europe/London': 'GMT Standard Time', - 'Europe/Luxembourg': 'W. Europe Standard Time', - 'Europe/Madrid': 'Romance Standard Time', - 'Europe/Malta': 'W. Europe Standard Time', - 'Europe/Mariehamn': 'FLE Standard Time', - 'Europe/Minsk': 'Kaliningrad Standard Time', - 'Europe/Monaco': 'W. Europe Standard Time', - 'Europe/Moscow': 'Russian Standard Time', - 'Europe/Oslo': 'W. Europe Standard Time', - 'Europe/Paris': 'Romance Standard Time', - 'Europe/Podgorica': 'Central Europe Standard Time', - 'Europe/Prague': 'Central Europe Standard Time', - 'Europe/Riga': 'FLE Standard Time', - 'Europe/Rome': 'W. Europe Standard Time', - 'Europe/Samara': 'Russian Standard Time', - 'Europe/San_Marino': 'W. Europe Standard Time', - 'Europe/Sarajevo': 'Central European Standard Time', - 'Europe/Simferopol': 'FLE Standard Time', - 'Europe/Skopje': 'Central European Standard Time', - 'Europe/Sofia': 'FLE Standard Time', - 'Europe/Stockholm': 'W. Europe Standard Time', - 'Europe/Tallinn': 'FLE Standard Time', - 'Europe/Tirane': 'Central Europe Standard Time', - 'Europe/Uzhgorod': 'FLE Standard Time', - 'Europe/Vaduz': 'W. Europe Standard Time', - 'Europe/Vatican': 'W. Europe Standard Time', - 'Europe/Vienna': 'W. Europe Standard Time', - 'Europe/Vilnius': 'FLE Standard Time', - 'Europe/Volgograd': 'Russian Standard Time', - 'Europe/Warsaw': 'Central European Standard Time', - 'Europe/Zagreb': 'Central European Standard Time', - 'Europe/Zaporozhye': 'FLE Standard Time', - 'Europe/Zurich': 'W. Europe Standard Time', - 'Indian/Antananarivo': 'E. Africa Standard Time', - 'Indian/Chagos': 'Central Asia Standard Time', - 'Indian/Christmas': 'SE Asia Standard Time', - 'Indian/Cocos': 'Myanmar Standard Time', - 'Indian/Comoro': 'E. Africa Standard Time', - 'Indian/Kerguelen': 'West Asia Standard Time', - 'Indian/Mahe': 'Mauritius Standard Time', - 'Indian/Maldives': 'West Asia Standard Time', - 'Indian/Mauritius': 'Mauritius Standard Time', - 'Indian/Mayotte': 'E. Africa Standard Time', - 'Indian/Reunion': 'Mauritius Standard Time', - 'MST7MDT': 'Mountain Standard Time', - 'PST8PDT': 'Pacific Standard Time', - 'Pacific/Apia': 'Samoa Standard Time', - 'Pacific/Auckland': 'New Zealand Standard Time', - 'Pacific/Efate': 'Central Pacific Standard Time', - 'Pacific/Enderbury': 'Tonga Standard Time', - 'Pacific/Fakaofo': 'Tonga Standard Time', - 'Pacific/Fiji': 'Fiji Standard Time', - 'Pacific/Funafuti': 'UTC+12', - 'Pacific/Galapagos': 'Central America Standard Time', - 'Pacific/Guadalcanal': 'Central Pacific Standard Time', - 'Pacific/Guam': 'West Pacific Standard Time', - 'Pacific/Honolulu': 'Hawaiian Standard Time', - 'Pacific/Johnston': 'Hawaiian Standard Time', - 'Pacific/Kosrae': 'Central Pacific Standard Time', - 'Pacific/Kwajalein': 'UTC+12', - 'Pacific/Majuro': 'UTC+12', - 'Pacific/Midway': 'UTC-11', - 'Pacific/Nauru': 'UTC+12', - 'Pacific/Niue': 'UTC-11', - 'Pacific/Noumea': 'Central Pacific Standard Time', - 'Pacific/Pago_Pago': 'UTC-11', - 'Pacific/Palau': 'Tokyo Standard Time', - 'Pacific/Ponape': 'Central Pacific Standard Time', - 'Pacific/Port_Moresby': 'West Pacific Standard Time', - 'Pacific/Rarotonga': 'Hawaiian Standard Time', - 'Pacific/Saipan': 'West Pacific Standard Time', - 'Pacific/Tahiti': 'Hawaiian Standard Time', - 'Pacific/Tarawa': 'UTC+12', - 'Pacific/Tongatapu': 'Tonga Standard Time', - 'Pacific/Truk': 'West Pacific Standard Time', - 'Pacific/Wake': 'UTC+12', - 'Pacific/Wallis': 'UTC+12' + "Africa/Abidjan": "Greenwich Standard Time", + "Africa/Accra": "Greenwich Standard Time", + "Africa/Addis_Ababa": "E. Africa Standard Time", + "Africa/Algiers": "W. Central Africa Standard Time", + "Africa/Asmera": "E. Africa Standard Time", + "Africa/Bamako": "Greenwich Standard Time", + "Africa/Bangui": "W. Central Africa Standard Time", + "Africa/Banjul": "Greenwich Standard Time", + "Africa/Bissau": "Greenwich Standard Time", + "Africa/Blantyre": "South Africa Standard Time", + "Africa/Brazzaville": "W. Central Africa Standard Time", + "Africa/Bujumbura": "South Africa Standard Time", + "Africa/Cairo": "Egypt Standard Time", + "Africa/Casablanca": "Morocco Standard Time", + "Africa/Ceuta": "Romance Standard Time", + "Africa/Conakry": "Greenwich Standard Time", + "Africa/Dakar": "Greenwich Standard Time", + "Africa/Dar_es_Salaam": "E. Africa Standard Time", + "Africa/Djibouti": "E. Africa Standard Time", + "Africa/Douala": "W. Central Africa Standard Time", + "Africa/El_Aaiun": "Morocco Standard Time", + "Africa/Freetown": "Greenwich Standard Time", + "Africa/Gaborone": "South Africa Standard Time", + "Africa/Harare": "South Africa Standard Time", + "Africa/Johannesburg": "South Africa Standard Time", + "Africa/Juba": "E. Africa Standard Time", + "Africa/Kampala": "E. Africa Standard Time", + "Africa/Khartoum": "Sudan Standard Time", + "Africa/Kigali": "South Africa Standard Time", + "Africa/Kinshasa": "W. Central Africa Standard Time", + "Africa/Lagos": "W. Central Africa Standard Time", + "Africa/Libreville": "W. Central Africa Standard Time", + "Africa/Lome": "Greenwich Standard Time", + "Africa/Luanda": "W. Central Africa Standard Time", + "Africa/Lubumbashi": "South Africa Standard Time", + "Africa/Lusaka": "South Africa Standard Time", + "Africa/Malabo": "W. Central Africa Standard Time", + "Africa/Maputo": "South Africa Standard Time", + "Africa/Maseru": "South Africa Standard Time", + "Africa/Mbabane": "South Africa Standard Time", + "Africa/Mogadishu": "E. Africa Standard Time", + "Africa/Monrovia": "Greenwich Standard Time", + "Africa/Nairobi": "E. Africa Standard Time", + "Africa/Ndjamena": "W. Central Africa Standard Time", + "Africa/Niamey": "W. Central Africa Standard Time", + "Africa/Nouakchott": "Greenwich Standard Time", + "Africa/Ouagadougou": "Greenwich Standard Time", + "Africa/Porto-Novo": "W. Central Africa Standard Time", + "Africa/Sao_Tome": "Sao Tome Standard Time", + "Africa/Timbuktu": "Greenwich Standard Time", + "Africa/Tripoli": "Libya Standard Time", + "Africa/Tunis": "W. Central Africa Standard Time", + "Africa/Windhoek": "Namibia Standard Time", + "America/Adak": "Aleutian Standard Time", + "America/Anchorage": "Alaskan Standard Time", + "America/Anguilla": "SA Western Standard Time", + "America/Antigua": "SA Western Standard Time", + "America/Araguaina": "Tocantins Standard Time", + "America/Argentina/La_Rioja": "Argentina Standard Time", + "America/Argentina/Rio_Gallegos": "Argentina Standard Time", + "America/Argentina/Salta": "Argentina Standard Time", + "America/Argentina/San_Juan": "Argentina Standard Time", + "America/Argentina/San_Luis": "Argentina Standard Time", + "America/Argentina/Tucuman": "Argentina Standard Time", + "America/Argentina/Ushuaia": "Argentina Standard Time", + "America/Aruba": "SA Western Standard Time", + "America/Asuncion": "Paraguay Standard Time", + "America/Atka": "Aleutian Standard Time", + "America/Bahia": "Bahia Standard Time", + "America/Bahia_Banderas": "Central Standard Time (Mexico)", + "America/Barbados": "SA Western Standard Time", + "America/Belem": "SA Eastern Standard Time", + "America/Belize": "Central America Standard Time", + "America/Blanc-Sablon": "SA Western Standard Time", + "America/Boa_Vista": "SA Western Standard Time", + "America/Bogota": "SA Pacific Standard Time", + "America/Boise": "Mountain Standard Time", + "America/Buenos_Aires": "Argentina Standard Time", + "America/Cambridge_Bay": "Mountain Standard Time", + "America/Campo_Grande": "Central Brazilian Standard Time", + "America/Cancun": "Eastern Standard Time (Mexico)", + "America/Caracas": "Venezuela Standard Time", + "America/Catamarca": "Argentina Standard Time", + "America/Cayenne": "SA Eastern Standard Time", + "America/Cayman": "SA Pacific Standard Time", + "America/Chicago": "Central Standard Time", + "America/Chihuahua": "Mountain Standard Time (Mexico)", + "America/Coral_Harbour": "SA Pacific Standard Time", + "America/Cordoba": "Argentina Standard Time", + "America/Costa_Rica": "Central America Standard Time", + "America/Creston": "US Mountain Standard Time", + "America/Cuiaba": "Central Brazilian Standard Time", + "America/Curacao": "SA Western Standard Time", + "America/Danmarkshavn": "UTC", + "America/Dawson": "Pacific Standard Time", + "America/Dawson_Creek": "US Mountain Standard Time", + "America/Denver": "Mountain Standard Time", + "America/Detroit": "Eastern Standard Time", + "America/Dominica": "SA Western Standard Time", + "America/Edmonton": "Mountain Standard Time", + "America/Eirunepe": "SA Pacific Standard Time", + "America/El_Salvador": "Central America Standard Time", + "America/Ensenada": "Pacific Standard Time (Mexico)", + "America/Fort_Nelson": "US Mountain Standard Time", + "America/Fortaleza": "SA Eastern Standard Time", + "America/Glace_Bay": "Atlantic Standard Time", + "America/Godthab": "Greenland Standard Time", + "America/Goose_Bay": "Atlantic Standard Time", + "America/Grand_Turk": "Turks And Caicos Standard Time", + "America/Grenada": "SA Western Standard Time", + "America/Guadeloupe": "SA Western Standard Time", + "America/Guatemala": "Central America Standard Time", + "America/Guayaquil": "SA Pacific Standard Time", + "America/Guyana": "SA Western Standard Time", + "America/Halifax": "Atlantic Standard Time", + "America/Havana": "Cuba Standard Time", + "America/Hermosillo": "US Mountain Standard Time", + "America/Indiana/Knox": "Central Standard Time", + "America/Indiana/Marengo": "US Eastern Standard Time", + "America/Indiana/Petersburg": "Eastern Standard Time", + "America/Indiana/Tell_City": "Central Standard Time", + "America/Indiana/Vevay": "US Eastern Standard Time", + "America/Indiana/Vincennes": "Eastern Standard Time", + "America/Indiana/Winamac": "Eastern Standard Time", + "America/Indianapolis": "US Eastern Standard Time", + "America/Inuvik": "Mountain Standard Time", + "America/Iqaluit": "Eastern Standard Time", + "America/Jamaica": "SA Pacific Standard Time", + "America/Jujuy": "Argentina Standard Time", + "America/Juneau": "Alaskan Standard Time", + "America/Kentucky/Monticello": "Eastern Standard Time", + "America/Knox_IN": "Central Standard Time", + "America/Kralendijk": "SA Western Standard Time", + "America/La_Paz": "SA Western Standard Time", + "America/Lima": "SA Pacific Standard Time", + "America/Los_Angeles": "Pacific Standard Time", + "America/Louisville": "Eastern Standard Time", + "America/Lower_Princes": "SA Western Standard Time", + "America/Maceio": "SA Eastern Standard Time", + "America/Managua": "Central America Standard Time", + "America/Manaus": "SA Western Standard Time", + "America/Marigot": "SA Western Standard Time", + "America/Martinique": "SA Western Standard Time", + "America/Matamoros": "Central Standard Time", + "America/Mazatlan": "Mountain Standard Time (Mexico)", + "America/Mendoza": "Argentina Standard Time", + "America/Menominee": "Central Standard Time", + "America/Merida": "Central Standard Time (Mexico)", + "America/Metlakatla": "Pacific Standard Time", + "America/Mexico_City": "Central Standard Time (Mexico)", + "America/Miquelon": "Saint Pierre Standard Time", + "America/Moncton": "Atlantic Standard Time", + "America/Monterrey": "Central Standard Time (Mexico)", + "America/Montevideo": "Montevideo Standard Time", + "America/Montreal": "Eastern Standard Time", + "America/Montserrat": "SA Western Standard Time", + "America/Nassau": "Eastern Standard Time", + "America/New_York": "Eastern Standard Time", + "America/Nipigon": "Eastern Standard Time", + "America/Nome": "Alaskan Standard Time", + "America/Noronha": "UTC-02", + "America/North_Dakota/Beulah": "Central Standard Time", + "America/North_Dakota/Center": "Central Standard Time", + "America/North_Dakota/New_Salem": "Central Standard Time", + "America/Ojinaga": "Mountain Standard Time", + "America/Panama": "SA Pacific Standard Time", + "America/Pangnirtung": "Eastern Standard Time", + "America/Paramaribo": "SA Eastern Standard Time", + "America/Phoenix": "US Mountain Standard Time", + "America/Port-au-Prince": "Haiti Standard Time", + "America/Port_of_Spain": "SA Western Standard Time", + "America/Porto_Acre": "SA Pacific Standard Time", + "America/Porto_Velho": "SA Western Standard Time", + "America/Puerto_Rico": "SA Western Standard Time", + "America/Punta_Arenas": "Magallanes Standard Time", + "America/Rainy_River": "Central Standard Time", + "America/Rankin_Inlet": "Central Standard Time", + "America/Recife": "SA Eastern Standard Time", + "America/Regina": "Canada Central Standard Time", + "America/Resolute": "Central Standard Time", + "America/Rio_Branco": "SA Pacific Standard Time", + "America/Santa_Isabel": "Pacific Standard Time (Mexico)", + "America/Santarem": "SA Eastern Standard Time", + "America/Santiago": "Pacific SA Standard Time", + "America/Santo_Domingo": "SA Western Standard Time", + "America/Sao_Paulo": "E. South America Standard Time", + "America/Scoresbysund": "Azores Standard Time", + "America/Shiprock": "Mountain Standard Time", + "America/Sitka": "Alaskan Standard Time", + "America/St_Barthelemy": "SA Western Standard Time", + "America/St_Johns": "Newfoundland Standard Time", + "America/St_Kitts": "SA Western Standard Time", + "America/St_Lucia": "SA Western Standard Time", + "America/St_Thomas": "SA Western Standard Time", + "America/St_Vincent": "SA Western Standard Time", + "America/Swift_Current": "Canada Central Standard Time", + "America/Tegucigalpa": "Central America Standard Time", + "America/Thule": "Atlantic Standard Time", + "America/Thunder_Bay": "Eastern Standard Time", + "America/Tijuana": "Pacific Standard Time (Mexico)", + "America/Toronto": "Eastern Standard Time", + "America/Tortola": "SA Western Standard Time", + "America/Vancouver": "Pacific Standard Time", + "America/Virgin": "SA Western Standard Time", + "America/Whitehorse": "Pacific Standard Time", + "America/Winnipeg": "Central Standard Time", + "America/Yakutat": "Alaskan Standard Time", + "America/Yellowknife": "Mountain Standard Time", + "Antarctica/Casey": "W. Australia Standard Time", + "Antarctica/Davis": "SE Asia Standard Time", + "Antarctica/DumontDUrville": "West Pacific Standard Time", + "Antarctica/Macquarie": "Central Pacific Standard Time", + "Antarctica/Mawson": "West Asia Standard Time", + "Antarctica/McMurdo": "New Zealand Standard Time", + "Antarctica/Palmer": "Magallanes Standard Time", + "Antarctica/Rothera": "SA Eastern Standard Time", + "Antarctica/South_Pole": "New Zealand Standard Time", + "Antarctica/Syowa": "E. Africa Standard Time", + "Antarctica/Vostok": "Central Asia Standard Time", + "Arctic/Longyearbyen": "W. Europe Standard Time", + "Asia/Aden": "Arab Standard Time", + "Asia/Almaty": "Central Asia Standard Time", + "Asia/Amman": "Jordan Standard Time", + "Asia/Anadyr": "Russia Time Zone 11", + "Asia/Aqtau": "West Asia Standard Time", + "Asia/Aqtobe": "West Asia Standard Time", + "Asia/Ashgabat": "West Asia Standard Time", + "Asia/Ashkhabad": "West Asia Standard Time", + "Asia/Atyrau": "West Asia Standard Time", + "Asia/Baghdad": "Arabic Standard Time", + "Asia/Bahrain": "Arab Standard Time", + "Asia/Baku": "Azerbaijan Standard Time", + "Asia/Bangkok": "SE Asia Standard Time", + "Asia/Barnaul": "Altai Standard Time", + "Asia/Beirut": "Middle East Standard Time", + "Asia/Bishkek": "Central Asia Standard Time", + "Asia/Brunei": "Singapore Standard Time", + "Asia/Calcutta": "India Standard Time", + "Asia/Chita": "Transbaikal Standard Time", + "Asia/Choibalsan": "Ulaanbaatar Standard Time", + "Asia/Chongqing": "China Standard Time", + "Asia/Chungking": "China Standard Time", + "Asia/Colombo": "Sri Lanka Standard Time", + "Asia/Dacca": "Bangladesh Standard Time", + "Asia/Damascus": "Syria Standard Time", + "Asia/Dhaka": "Bangladesh Standard Time", + "Asia/Dili": "Tokyo Standard Time", + "Asia/Dubai": "Arabian Standard Time", + "Asia/Dushanbe": "West Asia Standard Time", + "Asia/Famagusta": "GTB Standard Time", + "Asia/Gaza": "West Bank Standard Time", + "Asia/Harbin": "China Standard Time", + "Asia/Hebron": "West Bank Standard Time", + "Asia/Hong_Kong": "China Standard Time", + "Asia/Hovd": "W. Mongolia Standard Time", + "Asia/Irkutsk": "North Asia East Standard Time", + "Asia/Jakarta": "SE Asia Standard Time", + "Asia/Jayapura": "Tokyo Standard Time", + "Asia/Jerusalem": "Israel Standard Time", + "Asia/Kabul": "Afghanistan Standard Time", + "Asia/Kamchatka": "Russia Time Zone 11", + "Asia/Karachi": "Pakistan Standard Time", + "Asia/Kashgar": "Central Asia Standard Time", + "Asia/Katmandu": "Nepal Standard Time", + "Asia/Khandyga": "Yakutsk Standard Time", + "Asia/Krasnoyarsk": "North Asia Standard Time", + "Asia/Kuala_Lumpur": "Singapore Standard Time", + "Asia/Kuching": "Singapore Standard Time", + "Asia/Kuwait": "Arab Standard Time", + "Asia/Macao": "China Standard Time", + "Asia/Macau": "China Standard Time", + "Asia/Magadan": "Magadan Standard Time", + "Asia/Makassar": "Singapore Standard Time", + "Asia/Manila": "Singapore Standard Time", + "Asia/Muscat": "Arabian Standard Time", + "Asia/Nicosia": "GTB Standard Time", + "Asia/Novokuznetsk": "North Asia Standard Time", + "Asia/Novosibirsk": "N. Central Asia Standard Time", + "Asia/Omsk": "Omsk Standard Time", + "Asia/Oral": "West Asia Standard Time", + "Asia/Phnom_Penh": "SE Asia Standard Time", + "Asia/Pontianak": "SE Asia Standard Time", + "Asia/Pyongyang": "North Korea Standard Time", + "Asia/Qatar": "Arab Standard Time", + "Asia/Qostanay": "Central Asia Standard Time", + "Asia/Qyzylorda": "West Asia Standard Time", + "Asia/Rangoon": "Myanmar Standard Time", + "Asia/Riyadh": "Arab Standard Time", + "Asia/Saigon": "SE Asia Standard Time", + "Asia/Sakhalin": "Sakhalin Standard Time", + "Asia/Samarkand": "West Asia Standard Time", + "Asia/Seoul": "Korea Standard Time", + "Asia/Shanghai": "China Standard Time", + "Asia/Singapore": "Singapore Standard Time", + "Asia/Srednekolymsk": "Russia Time Zone 10", + "Asia/Taipei": "Taipei Standard Time", + "Asia/Tashkent": "West Asia Standard Time", + "Asia/Tbilisi": "Georgian Standard Time", + "Asia/Tehran": "Iran Standard Time", + "Asia/Tel_Aviv": "Israel Standard Time", + "Asia/Thimbu": "Bangladesh Standard Time", + "Asia/Thimphu": "Bangladesh Standard Time", + "Asia/Tokyo": "Tokyo Standard Time", + "Asia/Tomsk": "Tomsk Standard Time", + "Asia/Ujung_Pandang": "Singapore Standard Time", + "Asia/Ulaanbaatar": "Ulaanbaatar Standard Time", + "Asia/Ulan_Bator": "Ulaanbaatar Standard Time", + "Asia/Urumqi": "Central Asia Standard Time", + "Asia/Ust-Nera": "Vladivostok Standard Time", + "Asia/Vientiane": "SE Asia Standard Time", + "Asia/Vladivostok": "Vladivostok Standard Time", + "Asia/Yakutsk": "Yakutsk Standard Time", + "Asia/Yekaterinburg": "Ekaterinburg Standard Time", + "Asia/Yerevan": "Caucasus Standard Time", + "Atlantic/Azores": "Azores Standard Time", + "Atlantic/Bermuda": "Atlantic Standard Time", + "Atlantic/Canary": "GMT Standard Time", + "Atlantic/Cape_Verde": "Cape Verde Standard Time", + "Atlantic/Faeroe": "GMT Standard Time", + "Atlantic/Jan_Mayen": "W. Europe Standard Time", + "Atlantic/Madeira": "GMT Standard Time", + "Atlantic/Reykjavik": "Greenwich Standard Time", + "Atlantic/South_Georgia": "UTC-02", + "Atlantic/St_Helena": "Greenwich Standard Time", + "Atlantic/Stanley": "SA Eastern Standard Time", + "Australia/ACT": "AUS Eastern Standard Time", + "Australia/Adelaide": "Cen. Australia Standard Time", + "Australia/Brisbane": "E. Australia Standard Time", + "Australia/Broken_Hill": "Cen. Australia Standard Time", + "Australia/Canberra": "AUS Eastern Standard Time", + "Australia/Currie": "Tasmania Standard Time", + "Australia/Darwin": "AUS Central Standard Time", + "Australia/Eucla": "Aus Central W. Standard Time", + "Australia/Hobart": "Tasmania Standard Time", + "Australia/LHI": "Lord Howe Standard Time", + "Australia/Lindeman": "E. Australia Standard Time", + "Australia/Lord_Howe": "Lord Howe Standard Time", + "Australia/Melbourne": "AUS Eastern Standard Time", + "Australia/NSW": "AUS Eastern Standard Time", + "Australia/North": "AUS Central Standard Time", + "Australia/Perth": "W. Australia Standard Time", + "Australia/Queensland": "E. Australia Standard Time", + "Australia/South": "Cen. Australia Standard Time", + "Australia/Sydney": "AUS Eastern Standard Time", + "Australia/Tasmania": "Tasmania Standard Time", + "Australia/Victoria": "AUS Eastern Standard Time", + "Australia/West": "W. Australia Standard Time", + "Australia/Yancowinna": "Cen. Australia Standard Time", + "Brazil/Acre": "SA Pacific Standard Time", + "Brazil/DeNoronha": "UTC-02", + "Brazil/East": "E. South America Standard Time", + "Brazil/West": "SA Western Standard Time", + "CST6CDT": "Central Standard Time", + "Canada/Atlantic": "Atlantic Standard Time", + "Canada/Central": "Central Standard Time", + "Canada/Eastern": "Eastern Standard Time", + "Canada/Mountain": "Mountain Standard Time", + "Canada/Newfoundland": "Newfoundland Standard Time", + "Canada/Pacific": "Pacific Standard Time", + "Canada/Saskatchewan": "Canada Central Standard Time", + "Canada/Yukon": "Pacific Standard Time", + "Chile/Continental": "Pacific SA Standard Time", + "Chile/EasterIsland": "Easter Island Standard Time", + "Cuba": "Cuba Standard Time", + "EST5EDT": "Eastern Standard Time", + "Egypt": "Egypt Standard Time", + "Eire": "GMT Standard Time", + "Etc/GMT": "UTC", + "Etc/GMT+1": "Cape Verde Standard Time", + "Etc/GMT+10": "Hawaiian Standard Time", + "Etc/GMT+11": "UTC-11", + "Etc/GMT+12": "Dateline Standard Time", + "Etc/GMT+2": "UTC-02", + "Etc/GMT+3": "SA Eastern Standard Time", + "Etc/GMT+4": "SA Western Standard Time", + "Etc/GMT+5": "SA Pacific Standard Time", + "Etc/GMT+6": "Central America Standard Time", + "Etc/GMT+7": "US Mountain Standard Time", + "Etc/GMT+8": "UTC-08", + "Etc/GMT+9": "UTC-09", + "Etc/GMT-1": "W. Central Africa Standard Time", + "Etc/GMT-10": "West Pacific Standard Time", + "Etc/GMT-11": "Central Pacific Standard Time", + "Etc/GMT-12": "UTC+12", + "Etc/GMT-13": "UTC+13", + "Etc/GMT-14": "Line Islands Standard Time", + "Etc/GMT-2": "South Africa Standard Time", + "Etc/GMT-3": "E. Africa Standard Time", + "Etc/GMT-4": "Arabian Standard Time", + "Etc/GMT-5": "West Asia Standard Time", + "Etc/GMT-6": "Central Asia Standard Time", + "Etc/GMT-7": "SE Asia Standard Time", + "Etc/GMT-8": "Singapore Standard Time", + "Etc/GMT-9": "Tokyo Standard Time", + "Etc/UCT": "UTC", + "Etc/UTC": "UTC", + "Europe/Amsterdam": "W. Europe Standard Time", + "Europe/Andorra": "W. Europe Standard Time", + "Europe/Astrakhan": "Astrakhan Standard Time", + "Europe/Athens": "GTB Standard Time", + "Europe/Belfast": "GMT Standard Time", + "Europe/Belgrade": "Central Europe Standard Time", + "Europe/Berlin": "W. Europe Standard Time", + "Europe/Bratislava": "Central Europe Standard Time", + "Europe/Brussels": "Romance Standard Time", + "Europe/Bucharest": "GTB Standard Time", + "Europe/Budapest": "Central Europe Standard Time", + "Europe/Busingen": "W. Europe Standard Time", + "Europe/Chisinau": "E. Europe Standard Time", + "Europe/Copenhagen": "Romance Standard Time", + "Europe/Dublin": "GMT Standard Time", + "Europe/Gibraltar": "W. Europe Standard Time", + "Europe/Guernsey": "GMT Standard Time", + "Europe/Helsinki": "FLE Standard Time", + "Europe/Isle_of_Man": "GMT Standard Time", + "Europe/Istanbul": "Turkey Standard Time", + "Europe/Jersey": "GMT Standard Time", + "Europe/Kaliningrad": "Kaliningrad Standard Time", + "Europe/Kiev": "FLE Standard Time", + "Europe/Kirov": "Russian Standard Time", + "Europe/Lisbon": "GMT Standard Time", + "Europe/Ljubljana": "Central Europe Standard Time", + "Europe/London": "GMT Standard Time", + "Europe/Luxembourg": "W. Europe Standard Time", + "Europe/Madrid": "Romance Standard Time", + "Europe/Malta": "W. Europe Standard Time", + "Europe/Mariehamn": "FLE Standard Time", + "Europe/Minsk": "Belarus Standard Time", + "Europe/Monaco": "W. Europe Standard Time", + "Europe/Moscow": "Russian Standard Time", + "Europe/Oslo": "W. Europe Standard Time", + "Europe/Paris": "Romance Standard Time", + "Europe/Podgorica": "Central Europe Standard Time", + "Europe/Prague": "Central Europe Standard Time", + "Europe/Riga": "FLE Standard Time", + "Europe/Rome": "W. Europe Standard Time", + "Europe/Samara": "Russia Time Zone 3", + "Europe/San_Marino": "W. Europe Standard Time", + "Europe/Sarajevo": "Central European Standard Time", + "Europe/Saratov": "Saratov Standard Time", + "Europe/Simferopol": "Russian Standard Time", + "Europe/Skopje": "Central European Standard Time", + "Europe/Sofia": "FLE Standard Time", + "Europe/Stockholm": "W. Europe Standard Time", + "Europe/Tallinn": "FLE Standard Time", + "Europe/Tirane": "Central Europe Standard Time", + "Europe/Tiraspol": "E. Europe Standard Time", + "Europe/Ulyanovsk": "Astrakhan Standard Time", + "Europe/Uzhgorod": "FLE Standard Time", + "Europe/Vaduz": "W. Europe Standard Time", + "Europe/Vatican": "W. Europe Standard Time", + "Europe/Vienna": "W. Europe Standard Time", + "Europe/Vilnius": "FLE Standard Time", + "Europe/Volgograd": "Russian Standard Time", + "Europe/Warsaw": "Central European Standard Time", + "Europe/Zagreb": "Central European Standard Time", + "Europe/Zaporozhye": "FLE Standard Time", + "Europe/Zurich": "W. Europe Standard Time", + "GB": "GMT Standard Time", + "GB-Eire": "GMT Standard Time", + "GMT+0": "UTC", + "GMT-0": "UTC", + "GMT0": "UTC", + "Greenwich": "UTC", + "Hongkong": "China Standard Time", + "Iceland": "Greenwich Standard Time", + "Indian/Antananarivo": "E. Africa Standard Time", + "Indian/Chagos": "Central Asia Standard Time", + "Indian/Christmas": "SE Asia Standard Time", + "Indian/Cocos": "Myanmar Standard Time", + "Indian/Comoro": "E. Africa Standard Time", + "Indian/Kerguelen": "West Asia Standard Time", + "Indian/Mahe": "Mauritius Standard Time", + "Indian/Maldives": "West Asia Standard Time", + "Indian/Mauritius": "Mauritius Standard Time", + "Indian/Mayotte": "E. Africa Standard Time", + "Indian/Reunion": "Mauritius Standard Time", + "Iran": "Iran Standard Time", + "Israel": "Israel Standard Time", + "Jamaica": "SA Pacific Standard Time", + "Japan": "Tokyo Standard Time", + "Kwajalein": "UTC+12", + "Libya": "Libya Standard Time", + "MST7MDT": "Mountain Standard Time", + "Mexico/BajaNorte": "Pacific Standard Time (Mexico)", + "Mexico/BajaSur": "Mountain Standard Time (Mexico)", + "Mexico/General": "Central Standard Time (Mexico)", + "NZ": "New Zealand Standard Time", + "NZ-CHAT": "Chatham Islands Standard Time", + "Navajo": "Mountain Standard Time", + "PRC": "China Standard Time", + "PST8PDT": "Pacific Standard Time", + "Pacific/Apia": "Samoa Standard Time", + "Pacific/Auckland": "New Zealand Standard Time", + "Pacific/Bougainville": "Bougainville Standard Time", + "Pacific/Chatham": "Chatham Islands Standard Time", + "Pacific/Easter": "Easter Island Standard Time", + "Pacific/Efate": "Central Pacific Standard Time", + "Pacific/Enderbury": "UTC+13", + "Pacific/Fakaofo": "UTC+13", + "Pacific/Fiji": "Fiji Standard Time", + "Pacific/Funafuti": "UTC+12", + "Pacific/Galapagos": "Central America Standard Time", + "Pacific/Gambier": "UTC-09", + "Pacific/Guadalcanal": "Central Pacific Standard Time", + "Pacific/Guam": "West Pacific Standard Time", + "Pacific/Honolulu": "Hawaiian Standard Time", + "Pacific/Johnston": "Hawaiian Standard Time", + "Pacific/Kiritimati": "Line Islands Standard Time", + "Pacific/Kosrae": "Central Pacific Standard Time", + "Pacific/Kwajalein": "UTC+12", + "Pacific/Majuro": "UTC+12", + "Pacific/Marquesas": "Marquesas Standard Time", + "Pacific/Midway": "UTC-11", + "Pacific/Nauru": "UTC+12", + "Pacific/Niue": "UTC-11", + "Pacific/Norfolk": "Norfolk Standard Time", + "Pacific/Noumea": "Central Pacific Standard Time", + "Pacific/Pago_Pago": "UTC-11", + "Pacific/Palau": "Tokyo Standard Time", + "Pacific/Pitcairn": "UTC-08", + "Pacific/Ponape": "Central Pacific Standard Time", + "Pacific/Port_Moresby": "West Pacific Standard Time", + "Pacific/Rarotonga": "Hawaiian Standard Time", + "Pacific/Saipan": "West Pacific Standard Time", + "Pacific/Samoa": "UTC-11", + "Pacific/Tahiti": "Hawaiian Standard Time", + "Pacific/Tarawa": "UTC+12", + "Pacific/Tongatapu": "Tonga Standard Time", + "Pacific/Truk": "West Pacific Standard Time", + "Pacific/Wake": "UTC+12", + "Pacific/Wallis": "UTC+12", + "Poland": "Central European Standard Time", + "Portugal": "GMT Standard Time", + "ROC": "Taipei Standard Time", + "ROK": "Korea Standard Time", + "Singapore": "Singapore Standard Time", + "Turkey": "Turkey Standard Time", + "UCT": "UTC", + "US/Alaska": "Alaskan Standard Time", + "US/Aleutian": "Aleutian Standard Time", + "US/Arizona": "US Mountain Standard Time", + "US/Central": "Central Standard Time", + "US/Eastern": "Eastern Standard Time", + "US/Hawaii": "Hawaiian Standard Time", + "US/Indiana-Starke": "Central Standard Time", + "US/Michigan": "Eastern Standard Time", + "US/Mountain": "Mountain Standard Time", + "US/Pacific": "Pacific Standard Time", + "US/Samoa": "UTC-11", + "UTC": "UTC", + "Universal": "UTC", + "W-SU": "Russian Standard Time", + "Zulu": "UTC", } diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 4b4f956..f6c1def 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,16 +26,21 @@ """Nikola plugin categories.""" -from __future__ import absolute_import -import sys -import os -import re import io +import logging +import os +import sys +import typing -from yapsy.IPlugin import IPlugin +import doit from doit.cmd_base import Command as DoitCommand +from yapsy.IPlugin import IPlugin + +from .utils import LOGGER, first_line, get_logger, req_missing -from .utils import LOGGER, first_line +if typing.TYPE_CHECKING: + import nikola + import nikola.post __all__ = ( 'Command', @@ -43,22 +48,29 @@ __all__ = ( 'PageCompiler', 'RestExtension', 'MarkdownExtension', + 'MetadataExtractor', 'Task', 'TaskMultiplier', 'TemplateSystem', 'SignalHandler', 'ConfigPlugin', 'PostScanner', + 'Taxonomy', ) class BasePlugin(IPlugin): """Base plugin class.""" + logger = None + def set_site(self, site): """Set site, which is a Nikola instance.""" self.site = site self.inject_templates() + self.logger = get_logger(self.name) + if not site.debug: + self.logger.level = logging.INFO def inject_templates(self): """Inject 'templates/<engine>' (if exists) very early in the theme chain.""" @@ -90,10 +102,14 @@ class BasePlugin(IPlugin): class PostScanner(BasePlugin): """The scan method of these plugins is called by Nikola.scan_posts.""" - def scan(self): + def scan(self) -> 'typing.List[nikola.post.Post]': """Create a list of posts from some source. Returns a list of Post objects.""" raise NotImplementedError() + def supported_extensions(self) -> 'typing.Optional[typing.List]': + """Return a list of supported file extensions, or None if such a list isn't known beforehand.""" + return None + class Command(BasePlugin, DoitCommand): """Doit command implementation.""" @@ -118,7 +134,7 @@ class Command(BasePlugin, DoitCommand): DoitCommand.__init__(self, config, **kwargs) return self - def execute(self, options=None, args=None): + def execute(self, options=None, args=None) -> int: """Check if the command can run in the current environment, fail if needed, or call _execute.""" options = options or {} args = args or [] @@ -128,7 +144,7 @@ class Command(BasePlugin, DoitCommand): return False return self._execute(options, args) - def _execute(self, options, args): + def _execute(self, options, args) -> int: """Do whatever this command does. @param options (dict) with values from cmd_options @@ -154,7 +170,10 @@ def help(self): text.append(self.doc_description) return "\n".join(text) -DoitCommand.help = help + +# we need to patch DoitCommand.help with doit <0.31.0 +if doit.__version__ < (0, 31, 0): + DoitCommand.help = help class BaseTask(BasePlugin): @@ -166,11 +185,11 @@ class BaseTask(BasePlugin): # the others have to be specifie in the command line. is_default = True - def gen_tasks(self): + def gen_tasks(self) -> 'typing.List[dict]': """Generate tasks.""" raise NotImplementedError() - def group_task(self): + def group_task(self) -> dict: """Return dict for group task.""" return { 'basename': self.name, @@ -196,23 +215,23 @@ class TemplateSystem(BasePlugin): name = "dummy_templates" - def set_directories(self, directories, cache_folder): + def set_directories(self, directories: 'typing.List[str]', cache_folder: str): """Set the list of folders where templates are located and cache.""" raise NotImplementedError() - def template_deps(self, template_name): + def template_deps(self, template_name: str): """Return filenames which are dependencies for a template.""" raise NotImplementedError() - def get_deps(self, filename): + def get_deps(self, filename: str): """Return paths to dependencies for the template loaded from filename.""" raise NotImplementedError() - def get_string_deps(self, text): + def get_string_deps(self, text: str): """Find dependencies for a template string.""" raise NotImplementedError() - def render_template(self, template_name, output_name, context): + def render_template(self, template_name: str, output_name: str, context: 'typing.Dict[str, str]'): """Render template to a file using context. This must save the data to output_name *and* return it @@ -220,15 +239,15 @@ class TemplateSystem(BasePlugin): """ raise NotImplementedError() - def render_template_to_string(self, template, context): + def render_template_to_string(self, template: str, context: 'typing.Dict[str, str]') -> str: """Render template to a string using context.""" raise NotImplementedError() - def inject_directory(self, directory): + def inject_directory(self, directory: str): """Inject the directory with the lowest priority in the template search mechanism.""" raise NotImplementedError() - def get_template_path(self, template_name): + def get_template_path(self, template_name: str) -> str: """Get the path to a template or return None.""" raise NotImplementedError() @@ -238,7 +257,7 @@ class TaskMultiplier(BasePlugin): name = "dummy multiplier" - def process(self, task): + def process(self, task) -> list: """Examine task and create more tasks. Returns extra tasks only.""" return [] @@ -250,6 +269,9 @@ class PageCompiler(BasePlugin): friendly_name = '' demote_headers = False supports_onefile = True + use_dep_file = True # If set to false, the .dep file is never written and not automatically added as a target + supports_metadata = False + metadata_conditions = [] default_metadata = { 'title': '', 'slug': '', @@ -262,48 +284,75 @@ class PageCompiler(BasePlugin): } config_dependencies = [] - def _read_extra_deps(self, post): + def get_dep_filename(self, post: 'nikola.post.Post', lang: str) -> str: + """Return the .dep file's name for the given post and language.""" + return post.translated_base_path(lang) + '.dep' + + def _read_extra_deps(self, post: 'nikola.post.Post', lang: str) -> 'typing.List[str]': """Read contents of .dep file and return them as a list.""" - dep_path = post.base_path + '.dep' + dep_path = self.get_dep_filename(post, lang) if os.path.isfile(dep_path): - with io.open(dep_path, 'r+', encoding='utf8') as depf: + with io.open(dep_path, 'r+', encoding='utf-8-sig') as depf: deps = [l.strip() for l in depf.readlines()] return deps return [] - def register_extra_dependencies(self, post): + def register_extra_dependencies(self, post: 'nikola.post.Post'): """Add dependency to post object to check .dep file.""" - post.add_dependency(lambda: self._read_extra_deps(post), 'fragment') + def create_lambda(lang: str) -> 'typing.Callable': + # We create a lambda like this so we can pass `lang` to it, because if we didn’t + # add that function, `lang` would always be the last language in TRANSLATIONS. + # (See http://docs.python-guide.org/en/latest/writing/gotchas/#late-binding-closures) + return lambda: self._read_extra_deps(post, lang) + + for lang in self.site.config['TRANSLATIONS']: + post.add_dependency(create_lambda(lang), 'fragment', lang=lang) + + def get_extra_targets(self, post: 'nikola.post.Post', lang: str, dest: str) -> 'typing.List[str]': + """Return a list of extra targets for the render_posts task when compiling the post for the specified language.""" + if self.use_dep_file: + return [self.get_dep_filename(post, lang)] + else: + return [] + + def compile(self, source: str, dest: str, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" + raise NotImplementedError() - def compile_html(self, source, dest, is_two_file=False): - """Compile the source, save it on dest.""" + def compile_string(self, data: str, source_path=None, is_two_file=True, post=None, lang=None) -> str: + """Compile the source file into HTML strings (with shortcode support). + + Returns a tuple of at least two elements: HTML string [0] and shortcode dependencies [last]. + """ + # This function used to have some different APIs in different places. raise NotImplementedError() - def create_post(self, path, content=None, onefile=False, is_page=False, **kw): + def create_post(self, path: str, content=None, onefile=False, is_page=False, **kw): """Create post file with optional metadata.""" raise NotImplementedError() - def extension(self): - """The preferred extension for the output of this compiler.""" + def extension(self) -> str: + """Return the preferred extension for the output of this compiler.""" return ".html" - def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): + def read_metadata(self, post: 'nikola.post.Post', lang=None) -> 'typing.Dict[str, str]': """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. + def split_metadata(self, data: str, post=None, lang=None) -> (str, str): + """Split data from metadata in the raw post content.""" + if lang and post: + extractor = post.used_extractor[lang] + else: + import nikola.metadata_extractors + extractor = nikola.metadata_extractors.DEFAULT_EXTRACTOR - This splits in the first empty line that is NOT at the beginning - 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] + if isinstance(extractor, MetadataExtractor): + return extractor.split_metadata_from_text(data) + # Ouch! + return data, data - def get_compiler_extensions(self): + def get_compiler_extensions(self) -> list: """Activate all the compiler extension plugins for a given compiler and return them.""" plugins = [] for plugin_info in self.site.compiler_extensions: @@ -344,6 +393,73 @@ class MarkdownExtension(CompilerExtension): compiler_name = "markdown" +class MetadataExtractor(BasePlugin): + """Plugins that can extract meta information from post files.""" + + # Name of the extractor. (required) + name = "unknown" + # Where to get metadata from. (MetaSource; required) + source = None + # Priority of extractor. (MetaPriority; required) + priority = None + # List of tuples (MetaCondition, arg) with conditions used to select this extractor. + conditions = [] + # Regular expression used for splitting metadata, or None if not applicable. + split_metadata_re = None + # List of tuples (import name, pip name, friendly name) of Python packages required for this extractor. + requirements = [] + # Name of METADATA_MAPPING to use, if any. + map_from = None + # Whether or not the extractor supports writing metadata. + supports_write = False + + def _extract_metadata_from_text(self, source_text: str) -> 'typing.Dict[str, str]': + """Extract metadata from text.""" + raise NotImplementedError() + + def split_metadata_from_text(self, source_text: str) -> (str, str): + """Split text into metadata and content (both strings).""" + if self.split_metadata_re is None: + return source_text + else: + split_result = self.split_metadata_re.split(source_text.lstrip(), maxsplit=1) + if len(split_result) == 1: + return split_result[0], split_result[0] + else: + # Necessary? + return split_result[0], split_result[-1] + + def extract_text(self, source_text: str) -> 'typing.Dict[str, str]': + """Split file, return metadata and the content.""" + split = self.split_metadata_from_text(source_text) + if not split: + return {} + meta = self._extract_metadata_from_text(split[0]) + return meta + + def extract_filename(self, filename: str, lang: str) -> 'typing.Dict[str, str]': + """Extract metadata from filename.""" + return {} + + def write_metadata(self, metadata: 'typing.Dict[str, str]', comment_wrap=False) -> str: + """Write metadata in this extractor’s format. + + ``comment_wrap`` is either True, False, or a 2-tuple of comments to use for wrapping, if necessary. + If it’s set to True, defaulting to ``('<!--', '-->')`` is recommended. + + This function should insert comment markers (if applicable) and must insert trailing newlines. + """ + raise NotImplementedError() + + def check_requirements(self): + """Check if requirements for an extractor are satisfied.""" + for import_name, pip_name, friendly_name in self.requirements: + try: + __import__(import_name) + except ImportError: + req_missing([pip_name], "use {0} metadata".format(friendly_name), python=True, optional=False) + + class SignalHandler(BasePlugin): """Signal handlers.""" @@ -361,6 +477,12 @@ class ShortcodePlugin(BasePlugin): name = "dummy_shortcode_plugin" + def set_site(self, site): + """Set Nikola site.""" + self.site = site + site.register_shortcode(self.name, self.handler) + return super().set_site(site) + class Importer(Command): """Basic structure for importing data into Nikola. @@ -393,7 +515,7 @@ class Importer(Command): """Import the data into Nikola.""" raise NotImplementedError() - def generate_base_site(self, path): + def generate_base_site(self, path: str): """Create the base site.""" raise NotImplementedError() @@ -443,3 +565,333 @@ class Importer(Command): def save_post(self): """Save a post to disk.""" raise NotImplementedError() + + +class Taxonomy(BasePlugin): + """Taxonomy for posts. + + A taxonomy plugin allows to classify posts (see #2107) by + classification strings. Classification plugins must adjust + a set of options to determine certain aspects. + + The following options are class attributes with their default + values. These variables should be set in the class definition, + in the constructor or latest in the `set_site` function. + + classification_name = "taxonomy": + The classification name to be used for path handlers. + Must be overridden! + + overview_page_items_variable_name = "items": + When rendering the overview page, its template will have a list + of pairs + (friendly_name, link) + for the classifications available in a variable by this name. + + The template will also have a list + (friendly_name, link, post_count) + for the classifications available in a variable by the name + `overview_page_items_variable_name + '_with_postcount'`. + + overview_page_variable_name = "taxonomy": + When rendering the overview page, its template will have a list + of classifications available in a variable by this name. + + overview_page_hierarchy_variable_name = "taxonomy_hierarchy": + When rendering the overview page, its template will have a list + of tuples + (friendly_name, classification, classification_path, link, + indent_levels, indent_change_before, indent_change_after) + available in a variable by this name. These tuples can be used + to render the hierarchy as a tree. + + The template will also have a list + (friendly_name, classification, classification_path, link, + indent_levels, indent_change_before, indent_change_after, + number_of_children, post_count) + available in the variable by the name + `overview_page_hierarchy_variable_name + '_with_postcount'`. + + more_than_one_classifications_per_post = False: + If True, there can be more than one classification per post; in that case, + the classification data in the metadata is stored as a list. If False, + the classification data in the metadata is stored as a string, or None + when no classification is given. + + has_hierarchy = False: + Whether the classification has a hierarchy. + + include_posts_from_subhierarchies = False: + If True, the post list for a classification includes all posts with a + sub-classification (in case has_hierarchy is True). + + include_posts_into_hierarchy_root = False: + If True, include_posts_from_subhierarchies == True will also insert + posts into the post list for the empty hierarchy []. + + show_list_as_subcategories_list = False: + If True, for every classification which has at least one + subclassification, create a list of subcategories instead of a list/index + of posts. This is only used when has_hierarchy = True. The template + specified in subcategories_list_template will be used. If this is set + to True, it is recommended to set include_posts_from_subhierarchies to + True to get correct post counts. + + show_list_as_index = False: + Whether to show the posts for one classification as an index or + as a post list. + + subcategories_list_template = "taxonomy_list.tmpl": + The template to use for the subcategories list when + show_list_as_subcategories_list is True. + + template_for_single_list = "tagindex.tmpl": + The template to use for the post list for one classification. + + template_for_classification_overview = "list.tmpl": + The template to use for the classification overview page. + Set to None to avoid generating overviews. + + always_disable_atom = False: + Whether to always disable Atom feed generation. + + always_disable_rss = False: + Whether to always disable RSS feed generation. + + apply_to_posts = True: + Whether this classification applies to posts. + + apply_to_pages = False: + Whether this classification applies to pages. + + minimum_post_count_per_classification_in_overview = 1: + The minimum number of posts a classification must have to be listed in + the overview. + + omit_empty_classifications = False: + Whether post lists resp. indexes should be created for empty + classifications. + + add_other_languages_variable = False: + In case this is `True`, each classification page will get a list + of triples `(other_lang, other_classification, title)` of classifications + in other languages which should be linked. The list will be stored in the + variable `other_languages`. + + path_handler_docstrings: + A dictionary of docstrings for path handlers. See eg. nikola.py for + examples. Must be overridden, keys are "taxonomy_index", "taxonomy", + "taxonomy_atom", "taxonomy_rss" (but using classification_name instead + of "taxonomy"). If one of the values is False, the corresponding path + handler will not be created. + """ + + name = "dummy_taxonomy" + + # Adjust the following values in your plugin! + classification_name = "taxonomy" + overview_page_variable_name = "taxonomy" + overview_page_items_variable_name = "items" + overview_page_hierarchy_variable_name = "taxonomy_hierarchy" + more_than_one_classifications_per_post = False + has_hierarchy = False + include_posts_from_subhierarchies = False + include_posts_into_hierarchy_root = False + show_list_as_subcategories_list = False + show_list_as_index = False + subcategories_list_template = "taxonomy_list.tmpl" + template_for_single_list = "tagindex.tmpl" + template_for_classification_overview = "list.tmpl" + always_disable_atom = False + always_disable_rss = False + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = False + add_other_languages_variable = False + path_handler_docstrings = { + 'taxonomy_index': '', + 'taxonomy': '', + 'taxonomy_atom': '', + 'taxonomy_rss': '', + } + + def is_enabled(self, lang=None) -> bool: + """Return True if this taxonomy is enabled, or False otherwise. + + If lang is None, this determins whether the classification is + made at all. If lang is not None, this determines whether the + overview page and the classification lists are created for this + language. + """ + return True + + def get_implicit_classifications(self, lang: str) -> 'typing.List[str]': + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [] + + def classify(self, post: 'nikola.post.Post', lang: str) -> 'typing.Iterable[str]': + """Classify the given post for the given language. + + Must return a list or tuple of strings. + """ + raise NotImplementedError() + + def sort_posts(self, posts: 'typing.List[nikola.post.Post]', classification: str, lang: str): + """Sort the given list of posts. + + Allows the plugin to order the posts per classification as it wants. + The posts will be ordered by date (latest first) before calling + this function. This function must sort in-place. + """ + pass + + def sort_classifications(self, classifications: 'typing.List[str]', lang: str, level=None): + """Sort the given list of classification strings. + + Allows the plugin to order the classifications as it wants. The + classifications will be ordered by `natsort` before calling this + function. This function must sort in-place. + + For hierarchical taxonomies, the elements of the list are a single + path element of the path returned by `extract_hierarchy()`. The index + of the path element in the path will be provided in `level`. + """ + pass + + def get_classification_friendly_name(self, classification: str, lang: str, only_last_component=False) -> str: + """Extract a friendly name from the classification. + + The result of this function is usually displayed to the user, instead + of using the classification string. + + The argument `only_last_component` is only relevant to hierarchical + taxonomies. If it is set, the printable name should only describe the + last component of `classification` if possible. + """ + raise NotImplementedError() + + def get_overview_path(self, lang: str, dest_type='page') -> str: + """Return path for classification overview. + + This path handler for the classification overview must return one or + two values (in this order): + * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY; + * a string with values 'auto', 'always' or 'never', indicating whether + INDEX_FILE should be added or not. + + Note that this function must always return a list or tuple of strings; + the other return value is optional with default value `'auto'`. + + In case INDEX_FILE should potentially be added, the last element in the + returned path must have no extension, and the PRETTY_URLS config must + be ignored by this handler. The return value will be modified based on + the PRETTY_URLS and INDEX_FILE settings. + + `dest_type` can be either 'page', 'feed' (for Atom feed) or 'rss'. + """ + raise NotImplementedError() + + def get_path(self, classification: str, lang: str, dest_type='page') -> str: + """Return path to the classification page. + + This path handler for the given classification must return one to + three values (in this order): + * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY; + * a string with values 'auto', 'always' or 'never', indicating whether + INDEX_FILE should be added or not; + * an integer if a specific page of the index is to be targeted (will be + ignored for post lists), or `None` if the most current page is targeted. + + Note that this function must always return a list or tuple of strings; + the other two return values are optional with default values `'auto'` and + `None`. + + In case INDEX_FILE should potentially be added, the last element in the + returned path must have no extension, and the PRETTY_URLS config must + be ignored by this handler. The return value will be modified based on + the PRETTY_URLS and INDEX_FILE settings. + + `dest_type` can be either 'page', 'feed' (for Atom feed) or 'rss'. + + For hierarchical taxonomies, the result of extract_hierarchy is provided + as `classification`. For non-hierarchical taxonomies, the classification + string itself is provided as `classification`. + """ + raise NotImplementedError() + + def extract_hierarchy(self, classification: str) -> 'typing.List[str]': + """Given a classification, return a list of parts in the hierarchy. + + For non-hierarchical taxonomies, it usually suffices to return + `[classification]`. + """ + return [classification] + + def recombine_classification_from_hierarchy(self, hierarchy: 'typing.List[str]') -> str: + """Given a list of parts in the hierarchy, return the classification string. + + For non-hierarchical taxonomies, it usually suffices to return hierarchy[0]. + """ + return hierarchy[0] + + def provide_overview_context_and_uptodate(self, lang: str) -> str: + """Provide data for the context and the uptodate list for the classification overview. + + Must return a tuple of two dicts. The first is merged into the page's context, + the second will be put into the uptodate list of all generated tasks. + + Context must contain `title`. + """ + raise NotImplementedError() + + def provide_context_and_uptodate(self, classification: str, lang: str, node=None) -> 'typing.Tuple[typing.Dict]': + """Provide data for the context and the uptodate list for the list of the given classification. + + Must return a tuple of two dicts. The first is merged into the page's context, + the second will be put into the uptodate list of all generated tasks. + + For hierarchical taxonomies, node is the `hierarchy_utils.TreeNode` element + corresponding to the classification. + + Context must contain `title`, which should be something like 'Posts about <classification>'. + """ + raise NotImplementedError() + + def should_generate_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool: + """Only generates list of posts for classification if this function returns True.""" + return True + + def should_generate_atom_for_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool: + """Only generates Atom feed for list of posts for classification if this function returns True.""" + return self.should_generate_classification_page(classification, post_list, lang) + + def should_generate_rss_for_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool: + """Only generates RSS feed for list of posts for classification if this function returns True.""" + return self.should_generate_classification_page(classification, post_list, lang) + + def postprocess_posts_per_classification(self, posts_per_classification_per_language: 'typing.List[nikola.post.Post]', flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None) -> 'typing.List[nikola.post.Post]': + """Rearrange, modify or otherwise use the list of posts per classification and per language. + + For compatibility reasons, the list could be stored somewhere else as well. + + In case `has_hierarchy` is `True`, `flat_hierarchy_per_lang` is the flat + hierarchy consisting of `hierarchy_utils.TreeNode` elements, and + `hierarchy_lookup_per_lang` is the corresponding hierarchy lookup mapping + classification strings to `hierarchy_utils.TreeNode` objects. + """ + pass + + def get_other_language_variants(self, classification: str, lang: str, classifications_per_language: 'typing.List[str]') -> 'typing.List[str]': + """Return a list of variants of the same classification in other languages. + + Given a `classification` in a language `lang`, return a list of pairs + `(other_lang, other_classification)` with `lang != other_lang` such that + `classification` should be linked to `other_classification`. + + Classifications where links to other language versions makes no sense + should simply return an empty list. + + Provided is a set of classifications per language (`classifications_per_language`). + """ + return [] diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py index b83f43f..70c8c0d 100644 --- a/nikola/plugins/__init__.py +++ b/nikola/plugins/__init__.py @@ -1,5 +1,3 @@ # -*- 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 cf98ebc..3e6e21e 100644 --- a/nikola/plugins/basic_import.py +++ b/nikola/plugins/basic_import.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,21 +26,15 @@ """Mixin for importer plugins.""" -from __future__ import unicode_literals, print_function import io import csv import datetime import os -import sys -from pkg_resources import resource_filename - -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse # NOQA +from urllib.parse import urlparse from lxml import etree, html from mako.template import Template +from pkg_resources import resource_filename from nikola import utils @@ -90,7 +84,7 @@ class ImportMixin(object): src = (urlparse(k).path + 'index.html')[1:] dst = (urlparse(v).path) if src == index: - utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k)) + utils.LOGGER.warning("Can't do a redirect for: {0!r}".format(k)) else: redirections.append((src, dst)) return redirections @@ -101,8 +95,8 @@ class ImportMixin(object): os.system('nikola init -q ' + self.output_folder) else: self.import_into_existing_site = True - utils.LOGGER.notice('The folder {0} already exists - assuming that this is a ' - 'already existing Nikola site.'.format(self.output_folder)) + utils.LOGGER.warning('The folder {0} already exists - assuming that this is a ' + 'already existing Nikola site.'.format(self.output_folder)) filename = resource_filename('nikola', 'conf.py.in') # The 'strict_undefined=True' will give the missing symbol name if any, @@ -150,7 +144,7 @@ class ImportMixin(object): content = html.tostring(doc, encoding='utf8') except etree.ParserError: pass - if isinstance(content, utils.bytes_str): + if isinstance(content, bytes): content = content.decode('utf-8') compiler.create_post( filename, @@ -158,8 +152,7 @@ class ImportMixin(object): onefile=True, **headers) - @staticmethod - def write_metadata(filename, title, slug, post_date, description, tags, **kwargs): + def write_metadata(self, filename, title, slug, post_date, description, tags, **kwargs): """Write metadata to meta file.""" if not description: description = "" @@ -168,13 +161,13 @@ class ImportMixin(object): with io.open(filename, "w+", encoding="utf8") as fd: data = {'title': title, 'slug': slug, 'date': post_date, 'tags': ','.join(tags), 'description': description} data.update(kwargs) - fd.write(utils.write_metadata(data)) + fd.write(utils.write_metadata(data, site=self.site, comment_wrap=False)) @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+' + fmode = 'w+' with io.open(output_file, fmode) as fd: csv_writer = csv.writer(fd) for item in url_map.items(): diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py index 62d7086..cdd1560 100644 --- a/nikola/plugins/command/__init__.py +++ b/nikola/plugins/command/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin index 1081c78..a847e14 100644 --- a/nikola/plugins/command/auto.plugin +++ b/nikola/plugins/command/auto.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Automatically detect site changes, rebuild and optionally refresh a browser. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/auto/__init__.py b/nikola/plugins/command/auto/__init__.py index a82dc3e..6bedcac 100644 --- a/nikola/plugins/command/auto/__init__.py +++ b/nikola/plugins/command/auto/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,66 +26,55 @@ """Automatic rebuilds for Nikola.""" -from __future__ import print_function - -import json +import asyncio +import datetime import mimetypes import os import re +import stat import subprocess import sys -import time -try: - from urlparse import urlparse - from urllib2 import unquote -except ImportError: - from urllib.parse import urlparse, unquote # NOQA +import typing import webbrowser -from wsgiref.simple_server import make_server -import wsgiref.util + import pkg_resources -from blinker import signal +from nikola.plugin_categories import Command +from nikola.utils import dns_sd, req_missing, get_theme_path, makedirs + try: - from ws4py.websocket import WebSocket - from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler, WebSocketWSGIHandler - from ws4py.server.wsgiutils import WebSocketWSGIApplication - from ws4py.messaging import TextMessage + import aiohttp + from aiohttp import web + from aiohttp.web_urldispatcher import StaticResource + from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden, HTTPMovedPermanently + from aiohttp.web_response import Response + from aiohttp.web_fileresponse import FileResponse except ImportError: - WebSocket = object + aiohttp = web = None + StaticResource = HTTPNotFound = HTTPForbidden = Response = FileResponse = object + try: - import watchdog from watchdog.observers import Observer - from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler except ImportError: - watchdog = None - FileSystemEventHandler = object - PatternMatchingEventHandler = object + Observer = None -from nikola.plugin_categories import Command -from nikola.utils import dns_sd, 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') +REBUILDING_REFRESH_DELAY = 0.35 +IDLE_REFRESH_DELAY = 0.05 -ERROR_N = '''<html> -<head> -</head> -<boody> -ERROR {} -</body> -</html> -''' +if sys.platform == 'win32': + asyncio.set_event_loop(asyncio.ProactorEventLoop()) class CommandAuto(Command): """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" dns_sd = None + delta_last_rebuild = datetime.timedelta(milliseconds=100) + web_runner = None # type: web.AppRunner cmd_options = [ { @@ -94,7 +83,7 @@ class CommandAuto(Command): 'long': 'port', 'default': 8000, 'type': int, - 'help': 'Port nummber (default: 8000)', + 'help': 'Port number', }, { 'name': 'address', @@ -102,7 +91,7 @@ class CommandAuto(Command): 'long': 'address', 'type': str, 'default': '127.0.0.1', - 'help': 'Address to bind (default: 127.0.0.1 -- localhost)', + 'help': 'Address to bind', }, { 'name': 'browser', @@ -127,26 +116,50 @@ class CommandAuto(Command): 'type': bool, 'help': 'Disable the server, automate rebuilds only' }, + { + 'name': 'process', + 'short': 'n', + 'long': 'process', + 'default': 0, + 'type': int, + 'help': 'Number of subprocesses (nikola build argument)' + }, + { + 'name': 'parallel-type', + 'short': 'P', + 'long': 'parallel-type', + 'default': 'process', + 'type': str, + 'help': "Parallelization mode ('process' or 'thread', nikola build argument)" + }, ] def _execute(self, options, args): """Start the watcher.""" - self.logger = get_logger('auto', STDERR_HANDLER) - LRSocket.logger = self.logger - - if WebSocket is object and watchdog is None: - req_missing(['ws4py', 'watchdog'], 'use the "auto" command') - elif WebSocket is object: - req_missing(['ws4py'], 'use the "auto" command') - elif watchdog is None: + self.sockets = [] + self.rebuild_queue = asyncio.Queue() + self.reload_queue = asyncio.Queue() + self.last_rebuild = datetime.datetime.now() + self.is_rebuilding = False + + if aiohttp is None and Observer is None: + req_missing(['aiohttp', 'watchdog'], 'use the "auto" command') + elif aiohttp is None: + req_missing(['aiohttp'], 'use the "auto" command') + elif Observer is None: req_missing(['watchdog'], 'use the "auto" command') - self.cmd_arguments = ['nikola', 'build'] + if sys.argv[0].endswith('__main__.py'): + self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build'] + else: + self.nikola_cmd = [sys.argv[0], 'build'] + if self.site.configuration_filename != 'conf.py': - self.cmd_arguments.append('--conf=' + self.site.configuration_filename) + self.nikola_cmd.append('--conf=' + self.site.configuration_filename) - # Run an initial build so we are up-to-date - subprocess.call(self.cmd_arguments) + if options and options.get('process'): + self.nikola_cmd += ['--process={}'.format(options['process']), + '--parallel-type={}'.format(options['parallel-type'])] port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' @@ -155,7 +168,7 @@ class CommandAuto(Command): + 'script>')</script> </head>'''.format(port) - # Do not duplicate entries -- otherwise, multiple rebuilds are triggered + # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) @@ -167,12 +180,17 @@ class CommandAuto(Command): watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) + for item in self.site.config['IMAGE_FOLDERS']: + watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] + if not os.path.exists(out_folder): + makedirs(out_folder) + if options and options.get('browser'): browser = True else: @@ -181,289 +199,387 @@ class CommandAuto(Command): if options['ipv6']: dhost = '::' else: - dhost = None + dhost = '0.0.0.0' host = options['address'].strip('[').strip(']') or dhost + # Prepare asyncio event loop + # Required for subprocessing to work + loop = asyncio.get_event_loop() + + # Set debug setting + loop.set_debug(self.site.debug) + # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] - # Instantiate global observer - observer = Observer() if self.has_server: - # Watch output folders and trigger reloads - observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) + loop.run_until_complete(self.set_up_server(host, port, out_folder)) + + # Run an initial build so we are up-to-date. The server is running, but we are not watching yet. + loop.run_until_complete(self.run_initial_rebuild()) + + self.wd_observer = Observer() + # Watch output folders and trigger reloads + if self.has_server: + self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): - observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True) + self.wd_observer.schedule(NikolaEventHandler(self.queue_rebuild, loop), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) - observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) - - try: - self.logger.info("Watching files for changes...") - observer.start() - except KeyboardInterrupt: - pass + self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.queue_rebuild, loop), _conf_dn, recursive=False) + self.wd_observer.start() - parent = self + win_sleeper = None + # https://bugs.python.org/issue23057 (fixed in Python 3.8) + if sys.platform == 'win32' and sys.version_info < (3, 8): + win_sleeper = asyncio.ensure_future(windows_ctrlc_workaround()) - class Mixed(WebSocketWSGIApplication): - """A class that supports WS and HTTP protocols on the same port.""" + if not self.has_server: + self.logger.info("Watching for changes...") + # Run the event loop forever (no server mode). + try: + # Run rebuild queue + loop.run_until_complete(self.run_rebuild_queue()) - 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) + loop.run_forever() + except KeyboardInterrupt: + pass + finally: + if win_sleeper: + win_sleeper.cancel() + self.wd_observer.stop() + self.wd_observer.join() + loop.close() + return - 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) + 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("Serving on {0} ...".format(server_url)) - 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)) + if browser: + # Some browsers fail to load 0.0.0.0 (Issue #2755) + if host == '0.0.0.0': + server_url = "http://127.0.0.1:{0}/".format(port) + self.logger.info("Opening {0} in the default web browser...".format(server_url)) + webbrowser.open(server_url) - try: - self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) - ws.serve_forever() - except KeyboardInterrupt: - self.logger.info("Server is shutting down.") - if self.dns_sd: - self.dns_sd.Reset() - # 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) + # Run the event loop forever and handle shutdowns. + try: + # Run rebuild queue + rebuild_queue_fut = asyncio.ensure_future(self.run_rebuild_queue()) + reload_queue_fut = asyncio.ensure_future(self.run_reload_queue()) - def do_rebuild(self, event): + self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) + loop.run_forever() + except KeyboardInterrupt: + pass + finally: + self.logger.info("Server is shutting down.") + if win_sleeper: + win_sleeper.cancel() + if self.dns_sd: + self.dns_sd.Reset() + rebuild_queue_fut.cancel() + reload_queue_fut.cancel() + loop.run_until_complete(self.web_runner.cleanup()) + self.wd_observer.stop() + self.wd_observer.join() + loop.close() + + async def set_up_server(self, host: str, port: int, out_folder: str) -> None: + """Set up aiohttp server and start it.""" + webapp = web.Application() + webapp.router.add_get('/livereload.js', self.serve_livereload_js) + webapp.router.add_get('/robots.txt', self.serve_robots_txt) + webapp.router.add_route('*', '/livereload', self.websocket_handler) + resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder) + webapp.router.register_resource(resource) + webapp.on_shutdown.append(self.remove_websockets) + + self.web_runner = web.AppRunner(webapp) + await self.web_runner.setup() + website = web.TCPSite(self.web_runner, host, port) + await website.start() + + async def run_initial_rebuild(self) -> None: + """Run an initial rebuild.""" + await self._rebuild_site() + # If there are any clients, have them reload the root. + await self._send_reload_command(self.site.config['INDEX_FILE']) + + async def queue_rebuild(self, event) -> None: """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 + if sys.platform == 'win32': + # Windows hidden files support + is_hidden = os.stat(event_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN + else: + is_hidden = False + has_hidden_component = any(p.startswith('.') for p in event_path.split(os.sep)) + if (is_hidden or has_hidden_component or '__pycache__' in event_path or - event_path.endswith(('.pyc', '.pyo', '.pyd')) or - os.path.isdir(event_path)): # Skip on folders, these are usually duplicates + event_path.endswith(('.pyc', '.pyo', '.pyd', '_bak', '~')) or + event.is_directory): # 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: - self.logger.error(errord) - error_signal.send(error=errord) + + self.logger.debug('Queuing rebuild from {0}'.format(event_path)) + await self.rebuild_queue.put((datetime.datetime.now(), event_path)) + + async def run_rebuild_queue(self) -> None: + """Run rebuilds from a queue (Nikola can only build in a single instance).""" + while True: + date, event_path = await self.rebuild_queue.get() + if date < (self.last_rebuild + self.delta_last_rebuild): + self.logger.debug("Skipping rebuild from {0} (within delta)".format(event_path)) + continue + await self._rebuild_site(event_path) + + async def _rebuild_site(self, event_path: typing.Optional[str] = None) -> None: + """Rebuild the site.""" + self.is_rebuilding = True + self.last_rebuild = datetime.datetime.now() + if event_path: + self.logger.info('REBUILDING SITE (from {0})'.format(event_path)) else: - print(errord) + self.logger.info('REBUILDING SITE') - def do_refresh(self, event): - """Refresh the page.""" + p = await asyncio.create_subprocess_exec(*self.nikola_cmd, stderr=subprocess.PIPE) + exit_code = await p.wait() + out = (await p.stderr.read()).decode('utf-8') + + if exit_code != 0: + self.logger.error("Rebuild failed\n" + out) + await self.send_to_websockets({'command': 'alert', 'message': out}) + else: + self.logger.info("Rebuild successful\n" + out) + + self.is_rebuilding = False + + async def run_reload_queue(self) -> None: + """Send reloads from a queue to limit CPU usage.""" + while True: + p = await self.reload_queue.get() + self.logger.info('REFRESHING: {0}'.format(p)) + await self._send_reload_command(p) + if self.is_rebuilding: + await asyncio.sleep(REBUILDING_REFRESH_DELAY) + else: + await asyncio.sleep(IDLE_REFRESH_DELAY) + + async def _send_reload_command(self, path: str) -> None: + """Send a reload command.""" + await self.send_to_websockets({'command': 'reload', 'path': path, 'liveCSS': True}) + + async def reload_page(self, event) -> None: + """Reload 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'], *[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 p_uri.path.endswith('/'): # Redirect to avoid breakage - start_response('301 Moved Permanently', [('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; charset=UTF-8')]) - return ['User-Agent: *\nDisallow: /\n'.encode('utf-8')] - elif os.path.isfile(f_path): - with open(f_path, 'rb') as fd: - if mimetype.startswith('text/') or mimetype.endswith('+xml'): - start_response('200 OK', [('Content-type', "{0}; charset=UTF-8".format(mimetype))]) - else: - start_response('200 OK', [('Content-type', mimetype)]) - 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.file_filter(mimetype, fd.read())] - start_response('404 ERR', []) - return [self.file_filter('text/html', ERROR_N.format(404).format(uri).encode('utf-8'))] - - def file_filter(self, mimetype, data): - """Apply necessary changes to document before serving.""" - if mimetype == 'text/html': - 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 - if message['command'] == 'hello': # Handshake - response = { - 'command': 'hello', - 'protocols': [ - 'http://livereload.com/protocols/official-7', - ], - 'serverName': 'nikola-livereload', - } - elif message['command'] == 'info': # Someone connected - self.logger.info('****** Browser connected: {0}'.format(message.get('url'))) - self.logger.info('****** sending {0} pending messages'.format(len(pending))) - while pending: - msg = pending.pop() - self.logger.info('---> {0}'.format(msg.data)) - self.send(msg, msg.is_binary) - else: - response = { - 'command': 'alert', - 'message': 'HEY', - } - if response is not None: - response = json.dumps(response) - self.logger.info('---> {0}'.format(response)) - response = TextMessage(response) - self.send(response, response.is_binary) - - def notify(self, sender, path): - """Send reload requests to the client.""" - p = os.path.join('/', path) - message = { - 'command': 'reload', - 'liveCSS': True, - 'path': p, - } - response = json.dumps(message) - self.logger.info('---> {0}'.format(p)) - response = TextMessage(response) - if self.stream is None: # No client connected or whatever - pending.append(response) + if event: + event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path else: - self.send(response, response.is_binary) + event_path = self.site.config['OUTPUT_FOLDER'] + p = os.path.relpath(event_path, os.path.abspath(self.site.config['OUTPUT_FOLDER'])).replace(os.sep, '/') + await self.reload_queue.put(p) + + async def serve_livereload_js(self, request): + """Handle requests to /livereload.js and serve the JS file.""" + return FileResponse(LRJS_PATH) + + async def serve_robots_txt(self, request): + """Handle requests to /robots.txt.""" + return Response(body=b'User-Agent: *\nDisallow: /\n', content_type='text/plain', charset='utf-8') + + async def websocket_handler(self, request): + """Handle requests to /livereload and initiate WebSocket communication.""" + ws = web.WebSocketResponse() + await ws.prepare(request) + self.sockets.append(ws) + + while True: + msg = await ws.receive() + + self.logger.debug("Received message: {0}".format(msg)) + if msg.type == aiohttp.WSMsgType.TEXT: + message = msg.json() + if message['command'] == 'hello': + response = { + 'command': 'hello', + 'protocols': [ + 'http://livereload.com/protocols/official-7', + ], + 'serverName': 'Nikola Auto (livereload)', + } + await ws.send_json(response) + elif message['command'] != 'info': + self.logger.warning("Unknown command in message: {0}".format(message)) + elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING): + break + elif msg.type == aiohttp.WSMsgType.CLOSE: + self.logger.debug("Closing WebSocket") + await ws.close() + break + elif msg.type == aiohttp.WSMsgType.ERROR: + self.logger.error('WebSocket connection closed with exception {0}'.format(ws.exception())) + break + else: + self.logger.warning("Received unknown message: {0}".format(msg)) + + self.sockets.remove(ws) + self.logger.debug("WebSocket connection closed: {0}".format(ws)) + + return ws + + async def remove_websockets(self, app) -> None: + """Remove all websockets.""" + for ws in self.sockets: + await ws.close() + self.sockets.clear() + + async def send_to_websockets(self, message: dict) -> None: + """Send a message to all open WebSockets.""" + to_delete = [] + for ws in self.sockets: + if ws.closed: + to_delete.append(ws) + continue - def send_error(self, sender, error=None): - """Send reload requests to the client.""" - if self.stream is None: # No client connected or whatever - return - message = { - 'command': 'alert', - 'message': error, - } - response = json.dumps(message) - response = TextMessage(response) - if self.stream is None: # No client connected or whatever - pending.append(response) + try: + await ws.send_json(message) + if ws._close_code: + await ws.close() + to_delete.append(ws) + except RuntimeError as e: + if 'closed' in e.args[0]: + self.logger.warning("WebSocket {0} closed uncleanly".format(ws)) + to_delete.append(ws) + else: + raise + + for ws in to_delete: + self.sockets.remove(ws) + + +async def windows_ctrlc_workaround() -> None: + """Work around bpo-23057.""" + # https://bugs.python.org/issue23057 + while True: + await asyncio.sleep(1) + + +class IndexHtmlStaticResource(StaticResource): + """A StaticResource implementation that serves /index.html in directory roots.""" + + modify_html = True + snippet = "</head>" + + def __init__(self, modify_html=True, snippet="</head>", *args, **kwargs): + """Initialize a resource.""" + self.modify_html = modify_html + self.snippet = snippet + super().__init__(*args, **kwargs) + + async def _handle(self, request: 'web.Request') -> 'web.Response': + """Handle incoming requests (pass to handle_file).""" + filename = request.match_info['filename'] + return await self.handle_file(request, filename) + + async def handle_file(self, request: 'web.Request', filename: str, from_index=None) -> 'web.Response': + """Handle file requests.""" + try: + filepath = self._directory.joinpath(filename).resolve() + if not self._follow_symlinks: + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError) as error: + # relatively safe + raise HTTPNotFound() from error + except Exception as error: + # perm error or other kind! + request.app.logger.exception(error) + raise HTTPNotFound() from error + + # on opening a dir, load it's contents if allowed + if filepath.is_dir(): + if filename.endswith('/') or not filename: + ret = await self.handle_file(request, filename + 'index.html', from_index=filename) + else: + # Redirect and add trailing slash so relative links work (Issue #3140) + new_url = request.rel_url.path + '/' + if request.rel_url.query_string: + new_url += '?' + request.rel_url.query_string + raise HTTPMovedPermanently(new_url) + elif filepath.is_file(): + ct, encoding = mimetypes.guess_type(str(filepath)) + encoding = encoding or 'utf-8' + if ct == 'text/html' and self.modify_html: + if sys.version_info[0] == 3 and sys.version_info[1] <= 5: + # Python 3.4 and 3.5 do not accept pathlib.Path objects in calls to open() + filepath = str(filepath) + with open(filepath, 'r', encoding=encoding) as fh: + text = fh.read() + text = self.transform_html(text) + ret = Response(text=text, content_type=ct, charset=encoding) + else: + ret = FileResponse(filepath, chunk_size=self._chunk_size) + elif from_index: + filepath = self._directory.joinpath(from_index).resolve() + try: + return Response(text=self._directory_as_html(filepath), + content_type="text/html") + except PermissionError: + raise HTTPForbidden else: - self.send(response, response.is_binary) + raise HTTPNotFound + + return ret + + def transform_html(self, text: str) -> str: + """Apply some transforms to HTML content.""" + # Inject livereload.js + text = text.replace('</head>', self.snippet, 1) + # Disable <base> tag + text = re.sub(r'<base\s([^>]*)>', r'<!--base \g<1>-->', text, flags=re.IGNORECASE) + return text -class OurWatchHandler(FileSystemEventHandler): - """A Nikola-specific handler for Watchdog.""" +# Based on code from the 'hachiko' library by John Biesnecker — thanks! +# https://github.com/biesnecker/hachiko +class NikolaEventHandler: + """A Nikola-specific event handler for Watchdog. Based on code from hachiko.""" - def __init__(self, function): + def __init__(self, function, loop): """Initialize the handler.""" self.function = function - super(OurWatchHandler, self).__init__() + self.loop = loop - def on_any_event(self, event): - """Call the provided function on any event.""" - self.function(event) + async def on_any_event(self, event): + """Handle all file events.""" + await self.function(event) + def dispatch(self, event): + """Dispatch events to handler.""" + self.loop.call_soon_threadsafe(asyncio.ensure_future, self.on_any_event(event)) -class ConfigWatchHandler(FileSystemEventHandler): + +class ConfigEventHandler(NikolaEventHandler): """A Nikola-specific handler for Watchdog that handles the config file (as a workaround).""" - def __init__(self, configuration_filename, function): + def __init__(self, configuration_filename, function, loop): """Initialize the handler.""" self.configuration_filename = configuration_filename self.function = function + self.loop = loop - def on_any_event(self, event): - """Call the provided function on any event.""" + async def on_any_event(self, event): + """Handle file events if they concern the configuration file.""" 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 + await self.function(event) diff --git a/nikola/plugins/command/auto/livereload.js b/nikola/plugins/command/auto/livereload.js index b4cafb3..282dce5 120000 --- a/nikola/plugins/command/auto/livereload.js +++ b/nikola/plugins/command/auto/livereload.js @@ -1 +1 @@ -../../../../bower_components/livereload-js/dist/livereload.js
\ No newline at end of file +../../../../npm_assets/node_modules/livereload-js/dist/livereload.js
\ No newline at end of file diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py deleted file mode 100644 index 4808fdb..0000000 --- a/nikola/plugins/command/bootswatch_theme.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2016 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" - -from __future__ import print_function -import os -import requests - -from nikola.plugin_categories import Command -from nikola import utils - -LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER) - - -def _check_for_theme(theme, themes): - for t in themes: - if t.endswith(os.sep + theme): - return True - return False - - -class CommandBootswatchTheme(Command): - """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" - - name = "bootswatch_theme" - doc_usage = "[options]" - doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\ - " theme" - cmd_options = [ - { - 'name': 'name', - 'short': 'n', - 'long': 'name', - 'default': 'custom', - 'type': str, - 'help': 'New theme name (default: custom)', - }, - { - 'name': 'swatch', - 'short': 's', - 'default': '', - 'type': str, - 'help': 'Name of the swatch from bootswatch.com.' - }, - { - 'name': 'parent', - 'short': 'p', - 'long': 'parent', - 'default': 'bootstrap3', - 'help': 'Parent theme name (default: bootstrap3)', - }, - ] - - def _execute(self, options, args): - """Given a swatch name and a parent theme, creates a custom theme.""" - name = options['name'] - swatch = options['swatch'] - if not swatch: - LOGGER.error('The -s option is mandatory') - return 1 - parent = options['parent'] - version = '' - - # See if we need bootswatch for bootstrap v2 or v3 - themes = utils.get_theme_chain(parent, self.site.themes_dirs) - if not _check_for_theme('bootstrap3', themes) and not _check_for_theme('bootstrap3-jinja', themes): - version = '2' - elif not _check_for_theme('bootstrap', themes) and not _check_for_theme('bootstrap-jinja', themes): - LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap') - elif _check_for_theme('bootstrap3-gradients', themes) or _check_for_theme('bootstrap3-gradients-jinja', themes): - LOGGER.warn('"bootswatch_theme" doesn\'t work well with the bootstrap3-gradients family') - - LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent)) - utils.makedirs(os.path.join('themes', name, 'assets', 'css')) - for fname in ('bootstrap.min.css', 'bootstrap.css'): - url = 'https://bootswatch.com' - if version: - url += '/' + version - url = '/'.join((url, swatch, fname)) - LOGGER.info("Downloading: " + url) - r = requests.get(url) - if r.status_code > 299: - LOGGER.error('Error {} getting {}', r.status_code, url) - exit(1) - data = r.text - with open(os.path.join('themes', name, 'assets', 'css', fname), - 'wb+') as output: - output.write(data.encode('utf-8')) - - with open(os.path.join('themes', name, 'parent'), 'wb+') as output: - output.write(parent.encode('utf-8')) - LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use it.'.format(name)) diff --git a/nikola/plugins/command/check.plugin b/nikola/plugins/command/check.plugin index 6d2df82..bc6ede3 100644 --- a/nikola/plugins/command/check.plugin +++ b/nikola/plugins/command/check.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Check the generated site [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py index 0141a6b..cac6000 100644 --- a/nikola/plugins/command/check.py +++ b/nikola/plugins/command/check.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,25 +26,19 @@ """Check the generated site.""" -from __future__ import print_function -from collections import defaultdict +import logging import os import re import sys import time -import logbook -try: - from urllib import unquote - from urlparse import urlparse, urljoin, urldefrag -except ImportError: - from urllib.parse import unquote, urlparse, urljoin, urldefrag # NOQA +from collections import defaultdict +from urllib.parse import unquote, urlparse, urljoin, urldefrag -from doit.loader import generate_tasks import lxml.html import requests +from doit.loader import generate_tasks from nikola.plugin_categories import Command -from nikola.utils import get_logger, STDERR_HANDLER def _call_nikola_list(site, cache=None): @@ -104,7 +98,6 @@ class CommandCheck(Command): """Check the generated site.""" name = "check" - logger = None doc_usage = "[-v] (-l [--find-sources] [-r] | -f [--clean-files])" doc_purpose = "check links and files in the generated site" @@ -159,15 +152,13 @@ class CommandCheck(Command): def _execute(self, options, args): """Check the generated site.""" - self.logger = get_logger('check', STDERR_HANDLER) - if not options['links'] and not options['files'] and not options['clean']: print(self.help()) - return False + return 1 if options['verbose']: - self.logger.level = logbook.DEBUG + self.logger.level = logging.DEBUG else: - self.logger.level = logbook.NOTICE + self.logger.level = logging.WARNING failure = False if options['links']: failure |= self.scan_links(options['find_sources'], options['remote']) @@ -191,6 +182,7 @@ class CommandCheck(Command): self.existing_targets.add(self.site.config['SITE_URL']) self.existing_targets.add(self.site.config['BASE_URL']) url_type = self.site.config['URL_TYPE'] + atom_extension = self.site.config['ATOM_EXTENSION'] deps = {} if find_sources: @@ -205,7 +197,7 @@ class CommandCheck(Command): # Do not look at links in the cache, which are not parsed by # anyone and may result in false positives. Problems arise # with galleries, for example. Full rationale: (Issue #1447) - self.logger.notice("Ignoring {0} (in cache, links may be incorrect)".format(filename)) + self.logger.warning("Ignoring {0} (in cache, links may be incorrect)".format(filename)) return False if not os.path.exists(fname): @@ -213,7 +205,8 @@ class CommandCheck(Command): return False if '.html' == fname[-5:]: - d = lxml.html.fromstring(open(filename, 'rb').read()) + with open(filename, 'rb') as inf: + d = lxml.html.fromstring(inf.read()) extra_objs = lxml.html.fromstring('<html/>') # Turn elements with a srcset attribute into individual img elements with src attributes @@ -223,7 +216,7 @@ class CommandCheck(Command): extra_objs.append(lxml.etree.Element('img', src=srcset_item.strip().split(' ')[0])) link_elements = list(d.iterlinks()) + list(extra_objs.iterlinks()) # Extract links from XML formats to minimal HTML, allowing those to go through the link checks - elif '.atom' == filename[-5:]: + elif atom_extension == filename[-len(atom_extension):]: d = lxml.etree.parse(filename) link_elements = lxml.html.fromstring('<html/>') for elm in d.findall('*//{http://www.w3.org/2005/Atom}link'): @@ -257,13 +250,13 @@ class CommandCheck(Command): # Warn about links from https to http (mixed-security) if base_url.netloc == parsed.netloc and base_url.scheme == "https" and parsed.scheme == "http": - self.logger.warn("Mixed-content security for link in {0}: {1}".format(filename, target)) + self.logger.warning("Mixed-content security for link in {0}: {1}".format(filename, target)) # Link to an internal REDIRECTIONS page if target in self.internal_redirects: redir_status_code = 301 redir_target = [_dest for _target, _dest in self.site.config['REDIRECTIONS'] if urljoin('/', _target) == target][0] - self.logger.warn("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: 301]".format(redir_target, filename, target)) + self.logger.warning("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: 301]".format(redir_target, filename, target)) # Absolute links to other domains, skip # Absolute links when using only paths, skip. @@ -273,7 +266,7 @@ class CommandCheck(Command): continue if target in self.checked_remote_targets: # already checked this exact target if self.checked_remote_targets[target] in [301, 308]: - self.logger.warn("Remote link PERMANENTLY redirected in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) + self.logger.warning("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, 307]: self.logger.debug("Remote link temporarily redirected in {0}: {1} [HTTP: {2}]".format(filename, target, self.checked_remote_targets[target])) elif self.checked_remote_targets[target] > 399: @@ -281,7 +274,7 @@ class CommandCheck(Command): continue # Skip whitelisted targets - if any(re.search(_, target) for _ in self.whitelist): + if any(pattern.search(target) for pattern in self.whitelist): continue # Check the remote link works @@ -301,7 +294,7 @@ class CommandCheck(Command): 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)) + self.logger.warning("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.debug("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 @@ -315,7 +308,7 @@ class CommandCheck(Command): 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)) continue - self.logger.warn("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target)) + self.logger.warning("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target)) continue if url_type == 'rel_path': @@ -323,23 +316,44 @@ class CommandCheck(Command): target_filename = os.path.abspath( os.path.join(self.site.config['OUTPUT_FOLDER'], unquote(target.lstrip('/')))) else: # Relative path - unquoted_target = unquote(target).encode('utf-8') if sys.version_info.major >= 3 else unquote(target).decode('utf-8') + unquoted_target = unquote(target).encode('utf-8') target_filename = os.path.abspath( os.path.join(os.path.dirname(filename).encode('utf-8'), unquoted_target)) - elif url_type in ('full_path', 'absolute'): + else: + relative = False if url_type == 'absolute': # convert to 'full_path' case, ie url relative to root - url_rel_path = parsed.path[len(url_netloc_to_root):] + if parsed.path.startswith(url_netloc_to_root): + url_rel_path = parsed.path[len(url_netloc_to_root):] + else: + url_rel_path = parsed.path + if not url_rel_path.startswith('/'): + relative = True else: # convert to relative to base path - url_rel_path = target[len(url_netloc_to_root):] + if target.startswith(url_netloc_to_root): + url_rel_path = target[len(url_netloc_to_root):] + else: + url_rel_path = target + if not url_rel_path.startswith('/'): + relative = True if url_rel_path == '' or url_rel_path.endswith('/'): url_rel_path = urljoin(url_rel_path, self.site.config['INDEX_FILE']) - fs_rel_path = fs_relpath_from_url_path(url_rel_path) - target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], fs_rel_path) + if relative: + unquoted_target = unquote(target).encode('utf-8') + target_filename = os.path.abspath( + os.path.join(os.path.dirname(filename).encode('utf-8'), unquoted_target)) + else: + fs_rel_path = fs_relpath_from_url_path(url_rel_path) + target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], fs_rel_path) + + if isinstance(target_filename, str): + target_filename_str = target_filename + else: + target_filename_str = target_filename.decode("utf-8", errors="surrogateescape") - if any(re.search(x, target_filename) for x in self.whitelist): + if any(pattern.search(target_filename_str) for pattern in self.whitelist): continue elif target_filename not in self.existing_targets: @@ -348,11 +362,11 @@ class CommandCheck(Command): self.existing_targets.add(target_filename) else: rv = True - self.logger.warn("Broken link in {0}: {1}".format(filename, target)) + self.logger.warning("Broken link in {0}: {1}".format(filename, target)) if find_sources: - self.logger.warn("Possible sources:") - self.logger.warn("\n".join(deps[filename])) - self.logger.warn("===============================\n") + self.logger.warning("Possible sources:") + self.logger.warning("\n".join(deps[filename])) + self.logger.warning("===============================\n") except Exception as exc: self.logger.error(u"Error with: {0} {1}".format(filename, exc)) return rv @@ -363,6 +377,7 @@ class CommandCheck(Command): self.logger.debug("===============\n") self.logger.debug("{0} mode".format(self.site.config['URL_TYPE'])) failure = False + atom_extension = self.site.config['ATOM_EXTENSION'] # Maybe we should just examine all HTML files output_folder = self.site.config['OUTPUT_FOLDER'] @@ -374,7 +389,7 @@ class CommandCheck(Command): if '.html' == fname[-5:]: if self.analyze(fname, find_sources, check_remote): failure = True - if '.atom' == fname[-5:]: + if atom_extension == fname[-len(atom_extension):]: if self.analyze(fname, find_sources, False): failure = True if fname.endswith('sitemap.xml') or fname.endswith('sitemapindex.xml'): @@ -397,15 +412,15 @@ class CommandCheck(Command): if only_on_output: only_on_output.sort() - self.logger.warn("Files from unknown origins (orphans):") + self.logger.warning("Files from unknown origins (orphans):") for f in only_on_output: - self.logger.warn(f) + self.logger.warning(f) failure = True if only_on_input: only_on_input.sort() - self.logger.warn("Files not generated:") + self.logger.warning("Files not generated:") for f in only_on_input: - self.logger.warn(f) + self.logger.warning(f) if not failure: self.logger.debug("All files checked.") return failure @@ -434,6 +449,7 @@ class CommandCheck(Command): pass if warn_flag: - self.logger.warn('Some files or directories have been removed, your site may need rebuilding') + self.logger.warning('Some files or directories have been removed, your site may need rebuilding') + return True - return True + return False diff --git a/nikola/plugins/command/console.plugin b/nikola/plugins/command/console.plugin index 9bcc909..35e3585 100644 --- a/nikola/plugins/command/console.plugin +++ b/nikola/plugins/command/console.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Start a debugging python console [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py index c6a8376..b4342b4 100644 --- a/nikola/plugins/command/console.py +++ b/nikola/plugins/command/console.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Chris Warrick, Roberto Alsina and others. +# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,14 @@ """Start debugging console.""" -from __future__ import print_function, unicode_literals import os from nikola import __version__ from nikola.plugin_categories import Command -from nikola.utils import get_logger, STDERR_HANDLER, req_missing, Commands +from nikola.utils import get_logger, req_missing, Commands -LOGGER = get_logger('console', STDERR_HANDLER) +LOGGER = get_logger('console') class CommandConsole(Command): @@ -44,9 +43,9 @@ class CommandConsole(Command): shells = ['ipython', 'bpython', 'plain'] doc_purpose = "start an interactive Python console with access to your site" doc_description = """\ -The site engine is accessible as `site`, the config file as `conf`, and commands are available as `commands`. +The site engine is accessible as `site` and `nikola_site`, the config file as `conf`, and commands are available as `commands`. If there is no console to use specified (as -b, -i, -p) it tries IPython, then falls back to bpython, and finally falls back to the plain Python console.""" - header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site = site engine, commands = nikola commands)" + header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site, nikola_site = site engine, commands = nikola commands)" cmd_options = [ { 'name': 'bpython', @@ -72,19 +71,35 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f 'default': False, 'help': 'Use the plain Python interpreter', }, + { + 'name': 'command', + 'short': 'c', + 'long': 'command', + 'type': str, + 'default': None, + 'help': 'Run a single command', + }, + { + 'name': 'script', + 'short': 's', + 'long': 'script', + 'type': str, + 'default': None, + 'help': 'Execute a python script in the console context', + }, ] def ipython(self, willful=True): """Run an IPython shell.""" try: import IPython - except ImportError as e: + except ImportError: if willful: req_missing(['IPython'], 'use the IPython console') - raise e # That’s how _execute knows whether to try something else. + raise # That’s how _execute knows whether to try something else. else: site = self.context['site'] # NOQA - nikola_site = self.context['site'] # NOQA + nikola_site = self.context['nikola_site'] # NOQA conf = self.context['conf'] # NOQA commands = self.context['commands'] # NOQA IPython.embed(header=self.header.format('IPython')) @@ -93,10 +108,10 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f """Run a bpython shell.""" try: import bpython - except ImportError as e: + except ImportError: if willful: req_missing(['bpython'], 'use the bpython console') - raise e # That’s how _execute knows whether to try something else. + raise # That’s how _execute knows whether to try something else. else: bpython.embed(banner=self.header.format('bpython'), locals_=self.context) @@ -134,7 +149,13 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f 'nikola_site': self.site, 'commands': self.site.commands, } - if options['bpython']: + if options['command']: + exec(options['command'], None, self.context) + elif options['script']: + with open(options['script']) as inf: + code = compile(inf.read(), options['script'], 'exec') + exec(code, None, self.context) + elif options['bpython']: self.bpython(True) elif options['ipython']: self.ipython(True) diff --git a/nikola/plugins/command/default_config.plugin b/nikola/plugins/command/default_config.plugin new file mode 100644 index 0000000..af279f6 --- /dev/null +++ b/nikola/plugins/command/default_config.plugin @@ -0,0 +1,13 @@ +[Core] +name = default_config +module = default_config + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Show the default configuration. + +[Nikola] +PluginCategory = Command + diff --git a/nikola/plugins/command/default_config.py b/nikola/plugins/command/default_config.py new file mode 100644 index 0000000..036f4d1 --- /dev/null +++ b/nikola/plugins/command/default_config.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Show the default configuration.""" + +import sys + +import nikola.plugins.command.init +from nikola.plugin_categories import Command +from nikola.utils import get_logger + + +LOGGER = get_logger('default_config') + + +class CommandShowConfig(Command): + """Show the default configuration.""" + + name = "default_config" + + doc_usage = "" + needs_config = False + doc_purpose = "Print the default Nikola configuration." + cmd_options = [] + + def _execute(self, options=None, args=None): + """Show the default configuration.""" + try: + print(nikola.plugins.command.init.CommandInit.create_configuration_to_string()) + except Exception: + sys.stdout.buffer.write(nikola.plugins.command.init.CommandInit.create_configuration_to_string().encode('utf-8')) diff --git a/nikola/plugins/command/deploy.plugin b/nikola/plugins/command/deploy.plugin index 8bdc0e2..7cff28d 100644 --- a/nikola/plugins/command/deploy.plugin +++ b/nikola/plugins/command/deploy.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Deploy the site [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py index c2289e8..5273b58 100644 --- a/nikola/plugins/command/deploy.py +++ b/nikola/plugins/command/deploy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,19 +26,16 @@ """Deploy site.""" -from __future__ import print_function -import io -from datetime import datetime -from dateutil.tz import gettz -import dateutil -import os import subprocess import time +from datetime import datetime +import dateutil from blinker import signal +from dateutil.tz import gettz from nikola.plugin_categories import Command -from nikola.utils import get_logger, clean_before_deployment, STDERR_HANDLER +from nikola.utils import clean_before_deployment class CommandDeploy(Command): @@ -49,49 +46,28 @@ class CommandDeploy(Command): doc_usage = "[preset [preset...]]" doc_purpose = "deploy the site" doc_description = "Deploy the site by executing deploy commands from the presets listed on the command line. If no presets are specified, `default` is executed." - logger = None def _execute(self, command, args): """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') - # Get last-deploy from persistent state last_deploy = self.site.state.get('last_deploy') - if last_deploy is None: - # If there is a last-deploy saved, move it to the new state persistence thing - # FIXME: remove in Nikola 8 - if os.path.isfile(timestamp_path): - try: - with io.open(timestamp_path, 'r', encoding='utf8') as inf: - last_deploy = dateutil.parser.parse(inf.read()) - clean = False - except (IOError, Exception) as e: - self.logger.debug("Problem when reading `{0}`: {1}".format(timestamp_path, e)) - last_deploy = datetime(1970, 1, 1) - clean = True - os.unlink(timestamp_path) # Remove because from now on it's in state - else: # Just a default - last_deploy = datetime(1970, 1, 1) - clean = True - else: + if last_deploy is not None: last_deploy = dateutil.parser.parse(last_deploy) clean = False - if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo': - self.logger.warn("\nWARNING WARNING WARNING WARNING\n" - "You are deploying using the nikolademo Disqus account.\n" - "That means you will not be able to moderate the comments in your own site.\n" - "And is probably not what you want to do.\n" - "Think about it for 5 seconds, I'll wait :-)\n" - "(press Ctrl+C to abort)\n") + if self.site.config['COMMENT_SYSTEM'] and self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo': + self.logger.warning("\nWARNING WARNING WARNING WARNING\n" + "You are deploying using the nikolademo Disqus account.\n" + "That means you will not be able to moderate the comments in your own site.\n" + "And is probably not what you want to do.\n" + "Think about it for 5 seconds, I'll wait :-)\n" + "(press Ctrl+C to abort)\n") time.sleep(5) # Remove drafts and future posts if requested undeployed_posts = clean_before_deployment(self.site) if undeployed_posts: - self.logger.notice("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) + self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) if args: presets = args @@ -102,7 +78,7 @@ class CommandDeploy(Command): for preset in presets: try: self.site.config['DEPLOY_COMMANDS'][preset] - except: + except KeyError: self.logger.error('No such preset: {0}'.format(preset)) return 255 diff --git a/nikola/plugins/command/github_deploy.plugin b/nikola/plugins/command/github_deploy.plugin index 21e246c..fbdd3bf 100644 --- a/nikola/plugins/command/github_deploy.plugin +++ b/nikola/plugins/command/github_deploy.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Deploy the site to GitHub pages. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py index b5ad322..d2c1f3f 100644 --- a/nikola/plugins/command/github_deploy.py +++ b/nikola/plugins/command/github_deploy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2014-2016 Puneeth Chaganti and others. +# Copyright © 2014-2020 Puneeth Chaganti and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,14 +26,13 @@ """Deploy site to GitHub Pages.""" -from __future__ import print_function import os import subprocess 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, clean_before_deployment, STDERR_HANDLER +from nikola.utils import req_missing, clean_before_deployment from nikola.__main__ import main from nikola import __version__ @@ -54,6 +53,12 @@ def check_ghp_import_installed(): req_missing(['ghp-import2'], 'deploy the site to GitHub Pages') +class DeployFailedException(Exception): + """An internal exception for deployment errors.""" + + pass + + class CommandGitHubDeploy(Command): """Deploy site to GitHub Pages.""" @@ -63,11 +68,9 @@ class CommandGitHubDeploy(Command): doc_purpose = 'deploy the site to GitHub Pages' doc_description = dedent( """\ - This command can be used to deploy your site to GitHub Pages. - - It uses ghp-import to do this task. + This command can be used to deploy your site to GitHub Pages. It uses ghp-import to do this task. It also optionally commits to the source branch. - """ + Configuration help: https://getnikola.com/handbook.html#deploying-to-github""" ) cmd_options = [ { @@ -76,15 +79,12 @@ class CommandGitHubDeploy(Command): 'long': 'message', 'default': 'Nikola auto commit.', 'type': str, - 'help': 'Commit message (default: Nikola auto commit.)', + 'help': 'Commit message', }, ] - logger = None def _execute(self, options, args): """Run the deployment.""" - self.logger = get_logger(CommandGitHubDeploy.name, STDERR_HANDLER) - # Check if ghp-import is installed check_ghp_import_installed() @@ -102,12 +102,10 @@ class CommandGitHubDeploy(Command): # Remove drafts and future posts if requested (Issue #2406) undeployed_posts = clean_before_deployment(self.site) if undeployed_posts: - self.logger.notice("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) + self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) # Commit and push - self._commit_and_push(options['commit_message']) - - return + return self._commit_and_push(options['commit_message']) def _run_command(self, command, xfail=False): """Run a command that may or may not fail.""" @@ -122,7 +120,7 @@ class CommandGitHubDeploy(Command): 'Failed GitHub deployment -- command {0} ' 'returned {1}'.format(e.cmd, e.returncode) ) - raise SystemError(e.returncode) + raise DeployFailedException(e.returncode) def _commit_and_push(self, commit_first_line): """Commit all the files and push.""" @@ -145,9 +143,16 @@ class CommandGitHubDeploy(Command): if e != 0: self._run_command(['git', 'commit', '-am', commit_message]) else: - self.logger.notice('Nothing to commit to source branch.') + self.logger.info('Nothing to commit to source branch.') + + try: + source_commit = uni_check_output(['git', 'rev-parse', source]) + except subprocess.CalledProcessError: + try: + source_commit = uni_check_output(['git', 'rev-parse', 'HEAD']) + except subprocess.CalledProcessError: + source_commit = '?' - source_commit = uni_check_output(['git', 'rev-parse', source]) commit_message = ( '{0}\n\n' 'Source commit: {1}' @@ -161,7 +166,7 @@ class CommandGitHubDeploy(Command): if autocommit: self._run_command(['git', 'push', '-u', remote, source]) - except SystemError as e: + except DeployFailedException as e: return e.args[0] self.logger.info("Successful deployment") diff --git a/nikola/plugins/command/import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin index eab9d17..46df1ef 100644 --- a/nikola/plugins/command/import_wordpress.plugin +++ b/nikola/plugins/command/import_wordpress.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Import a wordpress site from a XML dump (requires markdown). [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py index 0b48583..5e2aee6 100644 --- a/nikola/plugins/command/import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,46 +26,45 @@ """Import a WordPress dump.""" -from __future__ import unicode_literals, print_function -import os -import re -import sys import datetime import io import json +import os +import re +import sys +from collections import defaultdict +from urllib.parse import urlparse, unquote + import requests from lxml import etree -from collections import defaultdict -try: - import html2text -except: - html2text = None +from nikola.plugin_categories import Command +from nikola import utils, hierarchy_utils +from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin, links +from nikola.plugins.command.init import ( + SAMPLE_CONF, prepare_config, + format_default_translations_config, + get_default_translations_dict +) try: - from urlparse import urlparse - from urllib import unquote + import html2text except ImportError: - from urllib.parse import urlparse, unquote # NOQA + html2text = None try: import phpserialize except ImportError: - phpserialize = None # NOQA + phpserialize = None -from nikola.plugin_categories import Command -from nikola import utils -from nikola.utils import req_missing, unicode_str -from nikola.plugins.basic_import import ImportMixin, links -from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN -from nikola.plugins.command.init import SAMPLE_CONF, prepare_config, format_default_translations_config - -LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) +LOGGER = utils.get_logger('import_wordpress') 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)) + LOGGER.info("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: @@ -148,15 +147,22 @@ class CommandImportWordpress(Command, ImportMixin): 'long': 'qtranslate', 'default': False, 'type': bool, - 'help': "Look for translations generated by qtranslate plugin", - # WARNING: won't recover translated titles that actually - # don't seem to be part of the wordpress XML export at the - # time of writing :( + 'help': """Look for translations generated by qtranslate plugin. +WARNING: a default wordpress export won't allow to recover title translations. +For this to be possible consider applying the hack suggested at +https://github.com/qtranslate/qtranslate-xt/issues/199 : + +In wp-admin/includes/export.php change +`echo apply_filters( 'the_title_rss', $post->post_title ); + +to +`echo apply_filters( 'the_title_export', $post->post_title ); +""" }, { 'name': 'translations_pattern', 'long': 'translations_pattern', - 'default': None, + 'default': DEFAULT_TRANSLATIONS_PATTERN, 'type': str, 'help': "The pattern for translation files names", }, @@ -259,9 +265,9 @@ class CommandImportWordpress(Command, ImportMixin): options['output_folder'] = args.pop(0) if args: - LOGGER.warn('You specified additional arguments ({0}). Please consider ' - 'putting these arguments before the filename if you ' - 'are running into problems.'.format(args)) + LOGGER.warning('You specified additional arguments ({0}). Please consider ' + 'putting these arguments before the filename if you ' + 'are running into problems.'.format(args)) self.onefile = options.get('one_file', False) @@ -307,7 +313,7 @@ class CommandImportWordpress(Command, ImportMixin): LOGGER.error("You can use at most one of the options --html2text, --transform-to-html and --transform-to-markdown.") return False if (self.html2text or self.transform_to_html or self.transform_to_markdown) and self.use_wordpress_compiler: - LOGGER.warn("It does not make sense to combine --use-wordpress-compiler with any of --html2text, --transform-to-html and --transform-to-markdown, as the latter convert all posts to HTML and the first option then affects zero posts.") + LOGGER.warning("It does not make sense to combine --use-wordpress-compiler with any of --html2text, --transform-to-html and --transform-to-markdown, as the latter convert all posts to HTML and the first option then affects zero posts.") if (self.html2text or self.transform_to_markdown) and not html2text: LOGGER.error("You need to install html2text via 'pip install html2text' before you can use the --html2text and --transform-to-markdown options.") @@ -339,14 +345,14 @@ class CommandImportWordpress(Command, ImportMixin): # 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_name = utils.html_unescape(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) + self._category_paths[cat] = hierarchy_utils.join_hierarchical_category_path(path) def _execute(self, options={}, args=[]): """Import a WordPress blog from an export file into a Nikola site.""" @@ -373,17 +379,12 @@ class CommandImportWordpress(Command, ImportMixin): if phpserialize is None: req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads') - channel = self.get_channel_from_file(self.wordpress_export_file) + export_file_preprocessor = modernize_qtranslate_tags if self.separate_qtranslate_content else None + channel = self.get_channel_from_file(self.wordpress_export_file, export_file_preprocessor) self._prepare(channel) conf_template = self.generate_base_site() - # If user has specified a custom pattern for translation files we - # need to fix the config - if self.translations_pattern: - self.context['TRANSLATIONS_PATTERN'] = self.translations_pattern - self.import_posts(channel) - self.context['TRANSLATIONS'] = format_default_translations_config( self.extra_languages) self.context['REDIRECTIONS'] = self.configure_redirections( @@ -397,7 +398,7 @@ class CommandImportWordpress(Command, ImportMixin): # Add tag redirects for tag in self.all_tags: try: - if isinstance(tag, utils.bytes_str): + if isinstance(tag, bytes): tag_str = tag.decode('utf8', 'replace') else: tag_str = tag @@ -420,9 +421,9 @@ class CommandImportWordpress(Command, ImportMixin): 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)) + LOGGER.warning("Make sure to install the WordPress page compiler via") + LOGGER.warning(" nikola plugin -i wordpress_compiler") + LOGGER.warning("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): @@ -438,9 +439,16 @@ class CommandImportWordpress(Command, ImportMixin): return b''.join(xml) @classmethod - def get_channel_from_file(cls, filename): - """Get channel from XML file.""" - tree = etree.fromstring(cls.read_xml_file(filename)) + def get_channel_from_file(cls, filename, xml_preprocessor=None): + """Get channel from XML file. + + An optional 'xml_preprocessor' allows to modify the xml + (typically to deal with variations in tags injected by some WP plugin) + """ + xml_string = cls.read_xml_file(filename) + if xml_preprocessor: + xml_string = xml_preprocessor(xml_string) + tree = etree.fromstring(xml_string) channel = tree.find('channel') return channel @@ -451,7 +459,10 @@ class CommandImportWordpress(Command, ImportMixin): context = SAMPLE_CONF.copy() self.lang = get_text_tag(channel, 'language', 'en')[:2] context['DEFAULT_LANG'] = self.lang - context['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN + # If user has specified a custom pattern for translation files we + # need to fix the config + context['TRANSLATIONS_PATTERN'] = self.translations_pattern + context['BLOG_TITLE'] = get_text_tag(channel, 'title', 'PUT TITLE HERE') context['BLOG_DESCRIPTION'] = get_text_tag( @@ -482,17 +493,17 @@ class CommandImportWordpress(Command, ImportMixin): PAGES = '(\n' for extension in extensions: POSTS += ' ("posts/*.{0}", "posts", "post.tmpl"),\n'.format(extension) - PAGES += ' ("pages/*.{0}", "pages", "story.tmpl"),\n'.format(extension) + PAGES += ' ("pages/*.{0}", "pages", "page.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' + 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 += ''' "wordpress": ['.wp'],''' + '\n' COMPILERS += '}' context['COMPILERS'] = COMPILERS @@ -503,12 +514,12 @@ class CommandImportWordpress(Command, ImportMixin): try: request = requests.get(url, auth=self.auth) if request.status_code >= 400: - LOGGER.warn("Downloading {0} to {1} failed with HTTP status code {2}".format(url, dst_path, request.status_code)) + LOGGER.warning("Downloading {0} to {1} failed with HTTP status code {2}".format(url, dst_path, request.status_code)) return with open(dst_path, 'wb+') as fd: fd.write(request.content) except requests.exceptions.ConnectionError as err: - LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err)) + LOGGER.warning("Downloading {0} to {1} failed: {2}".format(url, dst_path, err)) def import_attachment(self, item, wordpress_namespace): """Import an attachment to the site.""" @@ -549,14 +560,7 @@ class CommandImportWordpress(Command, ImportMixin): # 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')) + metadata = phpserialize.loads(meta_value.text.encode('utf-8')) meta_key = b'image_meta' size_key = b'sizes' @@ -583,6 +587,9 @@ class CommandImportWordpress(Command, ImportMixin): if ignore_zero and value == 0: return elif is_float: + # in some locales (like fr) and for old posts there may be a comma here. + if isinstance(value, bytes): + value = value.replace(b",", b".") value = float(value) if ignore_zero and value == 0: return @@ -775,7 +782,7 @@ class CommandImportWordpress(Command, ImportMixin): elif approved == 'spam' or approved == 'trash': pass else: - LOGGER.warn("Unknown comment approved status: {0}".format(approved)) + LOGGER.warning("Unknown comment approved status: {0}".format(approved)) parent = int(get_text_tag(comment, "{{{0}}}comment_parent".format(wordpress_namespace), 0)) if parent == 0: parent = None @@ -796,7 +803,7 @@ class CommandImportWordpress(Command, ImportMixin): """Write comment header line.""" if header_content is None: return - header_content = unicode_str(header_content).replace('\n', ' ') + header_content = str(header_content).replace('\n', ' ') line = '.. ' + header_field + ': ' + header_content + '\n' fd.write(line.encode('utf8')) @@ -813,6 +820,16 @@ class CommandImportWordpress(Command, ImportMixin): write_header_line(fd, "wordpress_user_id", comment["user_id"]) fd.write(('\n' + comment['content']).encode('utf8')) + def _create_meta_and_content_filenames(self, slug, extension, lang, default_language, translations_config): + out_meta_filename = slug + '.meta' + out_content_filename = slug + '.' + extension + if lang and lang != default_language: + out_meta_filename = utils.get_translation_candidate(translations_config, + out_meta_filename, lang) + out_content_filename = utils.get_translation_candidate(translations_config, + out_content_filename, lang) + return out_meta_filename, out_content_filename + def _create_metadata(self, status, excerpt, tags, categories, post_name=None): """Create post metadata.""" other_meta = {'wp-status': status} @@ -824,16 +841,16 @@ class CommandImportWordpress(Command, ImportMixin): if text in self._category_paths: cats.append(self._category_paths[text]) else: - cats.append(utils.join_hierarchical_category_path([text])) + cats.append(hierarchy_utils.join_hierarchical_category_path([utils.html_unescape(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 + LOGGER.warning(('Post "{0}" has more than one category! ' + + 'Will only use the first one.').format(post_name)) + tags_cats = [utils.html_unescape(tag) for tag in tags] else: - tags_cats = tags + categories + tags_cats = [utils.html_unescape(tag) for tag in tags + categories] return tags_cats, other_meta _tag_sanitize_map = {True: {}, False: {}} @@ -847,7 +864,7 @@ class CommandImportWordpress(Command, ImportMixin): previous = self._tag_sanitize_map[is_category][tag.lower()] if self.tag_saniziting_strategy == 'first': if tag != previous[0]: - LOGGER.warn("Changing spelling of {0} name '{1}' to {2}.".format('category' if is_category else 'tag', tag, previous[0])) + LOGGER.warning("Changing spelling of {0} name '{1}' to {2}.".format('category' if is_category else 'tag', tag, previous[0])) return previous[0] else: LOGGER.error("Unknown tag sanitizing strategy '{0}'!".format(self.tag_saniziting_strategy)) @@ -873,7 +890,7 @@ class CommandImportWordpress(Command, ImportMixin): path = unquote(parsed.path.strip('/')) try: - if isinstance(path, utils.bytes_str): + if isinstance(path, bytes): path = path.decode('utf8', 'replace') else: path = path @@ -925,17 +942,19 @@ class CommandImportWordpress(Command, ImportMixin): tags = [] categories = [] + post_status = 'published' + has_math = "no" if status == 'trash': - LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title)) + LOGGER.warning('Trashed post "{0}" will not be imported.'.format(title)) return False elif status == 'private': - tags.append('private') is_draft = False is_private = True + post_status = 'private' elif status != 'publish': - tags.append('draft') is_draft = True is_private = False + post_status = 'draft' else: is_draft = False is_private = False @@ -953,7 +972,7 @@ class CommandImportWordpress(Command, ImportMixin): tags.append(text) if '$latex' in content: - tags.append('mathjax') + has_math = "yes" for i, cat in enumerate(categories[:]): cat = self._sanitize(cat, True) @@ -974,52 +993,56 @@ class CommandImportWordpress(Command, ImportMixin): post_format = 'wp' if is_draft and self.exclude_drafts: - LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) + LOGGER.warning('Draft "{0}" will not be imported.'.format(title)) return False elif is_private and self.exclude_privates: - LOGGER.notice('Private post "{0}" will not be imported.'.format(title)) + LOGGER.warning('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 + '.html').replace(os.sep, '/') - if hasattr(self, "separate_qtranslate_content") \ - and self.separate_qtranslate_content: - content_translations = separate_qtranslate_content(content) + default_language = self.context["DEFAULT_LANG"] + if self.separate_qtranslate_content: + content_translations = separate_qtranslate_tagged_langs(content) + title_translations = separate_qtranslate_tagged_langs(title) else: content_translations = {"": content} - default_language = self.context["DEFAULT_LANG"] + title_translations = {"": title} + # in case of mistmatch between the languages found in the title and in the content + default_title = title_translations.get(default_language, title) + extra_languages = [lang for lang in content_translations.keys() if lang not in ("", default_language)] + for extra_lang in extra_languages: + self.extra_languages.add(extra_lang) + translations_dict = get_default_translations_dict(default_language, extra_languages) + current_translations_config = { + "DEFAULT_LANG": default_language, + "TRANSLATIONS": translations_dict, + "TRANSLATIONS_PATTERN": self.context["TRANSLATIONS_PATTERN"] + } for lang, content in content_translations.items(): try: content, extension, rewrite_html = self.transform_content(content, post_format, attachments) - except: + except Exception: 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 + '.' + extension - else: - out_content_filename \ - = utils.get_translation_candidate(self.context, - slug + "." + extension, lang) - self.extra_languages.add(lang) - meta_slug = slug - else: - out_meta_filename = slug + '.meta' - out_content_filename = slug + '.' + extension - meta_slug = slug + + out_meta_filename, out_content_filename = self._create_meta_and_content_filenames( + slug, extension, lang, default_language, current_translations_config) + tags, other_meta = self._create_metadata(status, excerpt, tags, categories, post_name=os.path.join(out_folder, slug)) - + current_title = title_translations.get(lang, default_title) meta = { - "title": title, - "slug": meta_slug, + "title": current_title, + "slug": slug, "date": post_date, "description": description, "tags": ','.join(tags), + "status": post_status, + "has_math": has_math, } meta.update(other_meta) if self.onefile: @@ -1033,7 +1056,7 @@ class CommandImportWordpress(Command, ImportMixin): else: self.write_metadata(os.path.join(self.output_folder, out_folder, out_meta_filename), - title, meta_slug, post_date, description, tags, **other_meta) + current_title, slug, post_date, description, tags, **other_meta) self.write_content( os.path.join(self.output_folder, out_folder, out_content_filename), @@ -1053,8 +1076,8 @@ class CommandImportWordpress(Command, ImportMixin): return (out_folder, slug) else: - LOGGER.warn(('Not going to import "{0}" because it seems to contain' - ' no content.').format(title)) + LOGGER.warning(('Not going to import "{0}" because it seems to contain' + ' no content.').format(title)) return False def _extract_item_info(self, item): @@ -1080,7 +1103,7 @@ class CommandImportWordpress(Command, ImportMixin): 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'])) + LOGGER.warning("Attachment #{0} ({1}) has no parent!".format(post_id, data['files'])) def write_attachments_info(self, path, attachments): """Write attachments info file.""" @@ -1118,8 +1141,8 @@ class CommandImportWordpress(Command, ImportMixin): 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()])) + LOGGER.warning(("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): @@ -1133,15 +1156,20 @@ def get_text_tag(tag, name, default): return default -def separate_qtranslate_content(text): - """Parse the content of a wordpress post or page and separate qtranslate languages. +def separate_qtranslate_tagged_langs(text): + """Parse the content of a wordpress post or page and separate languages. + + For qtranslateX tags: [:LL]blabla[:] - qtranslate tags: <!--:LL-->blabla<!--:--> + Note: qtranslate* plugins had a troubled history and used various + tags over time, application of the 'modernize_qtranslate_tags' + function is required for this function to handle most of the legacy + cases. """ - # TODO: uniformize qtranslate tags <!--/en--> => <!--:--> - qt_start = "<!--:" - qt_end = "-->" - qt_end_with_lang_len = 5 + qt_start = "[:" + qt_end = "]" + qt_end_len = len(qt_end) + qt_end_with_lang_len = qt_end_len + 2 qt_chunks = text.split(qt_start) content_by_lang = {} common_txt_list = [] @@ -1153,9 +1181,9 @@ def separate_qtranslate_content(text): # be some piece of common text or tags, or just nothing lang = "" # default language c = c.lstrip(qt_end) - if not c: + if not c.strip(): continue - elif c[2:].startswith(qt_end): + elif c[2:qt_end_with_lang_len].startswith(qt_end): # a language specific section (with language code at the begining) lang = c[:2] c = c[qt_end_with_lang_len:] @@ -1176,3 +1204,26 @@ def separate_qtranslate_content(text): for l in content_by_lang.keys(): content_by_lang[l] = " ".join(content_by_lang[l]) return content_by_lang + + +def modernize_qtranslate_tags(xml_bytes): + """ + Uniformize the "tag" used by various version of qtranslate. + + The resulting byte string will only contain one set of qtranslate tags + (namely [:LG] and [:]), older ones being converted to new ones. + """ + old_start_lang = re.compile(b"<!--:?(\\w{2})-->") + new_start_lang = b"[:\\1]" + old_end_lang = re.compile(b"<!--(/\\w{2}|:)-->") + new_end_lang = b"[:]" + title_match = re.compile(b"<title>(.*?)</title>") + modern_starts = old_start_lang.sub(new_start_lang, xml_bytes) + modernized_bytes = old_end_lang.sub(new_end_lang, modern_starts) + + def title_escape(match): + title = match.group(1) + title = title.replace(b"&", b"&").replace(b"<", b"<").replace(b">", b">") + return b"<title>" + title + b"</title>" + fixed_bytes = title_match.sub(title_escape, modernized_bytes) + return fixed_bytes diff --git a/nikola/plugins/command/init.plugin b/nikola/plugins/command/init.plugin index a8b1523..6ee27d3 100644 --- a/nikola/plugins/command/init.plugin +++ b/nikola/plugins/command/init.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create a new site. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py index 3d6669c..0026edc 100644 --- a/nikola/plugins/command/init.py +++ b/nikola/plugins/command/init.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,28 +26,28 @@ """Create a new site.""" -from __future__ import print_function, unicode_literals -import os -import shutil +import datetime import io import json +import os +import shutil import textwrap -import datetime import unidecode +from urllib.parse import urlsplit, urlunsplit + import dateutil.tz import dateutil.zoneinfo from mako.template import Template from pkg_resources import resource_filename -import tarfile import nikola -from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN, DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_FEED_READ_MORE_LINK, LEGAL_VALUES, urlsplit, urlunsplit +from nikola.nikola import DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_FEED_READ_MORE_LINK, LEGAL_VALUES from nikola.plugin_categories import Command -from nikola.utils import ask, ask_yesno, get_logger, makedirs, STDERR_HANDLER, load_messages +from nikola.utils import ask, ask_yesno, get_logger, makedirs, load_messages from nikola.packages.tzlocal import get_localzone -LOGGER = get_logger('init', STDERR_HANDLER) +LOGGER = get_logger('init') SAMPLE_CONF = { 'BLOG_AUTHOR': "Your Name", @@ -55,50 +55,51 @@ SAMPLE_CONF = { 'SITE_URL': "https://example.com/", 'BLOG_EMAIL': "joe@demo.site", 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", - 'PRETTY_URLS': False, - 'STRIP_INDEXES': False, + 'PRETTY_URLS': True, + 'STRIP_INDEXES': True, 'DEFAULT_LANG': "en", 'TRANSLATIONS': """{ DEFAULT_LANG: "", # Example for another language: # "es": "./es", }""", - 'THEME': 'bootstrap3', + 'THEME': LEGAL_VALUES['DEFAULT_THEME'], '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, 'FEED_READ_MORE_LINK': DEFAULT_FEED_READ_MORE_LINK, 'POSTS': """( ("posts/*.rst", "posts", "post.tmpl"), + ("posts/*.md", "posts", "post.tmpl"), ("posts/*.txt", "posts", "post.tmpl"), ("posts/*.html", "posts", "post.tmpl"), )""", 'PAGES': """( - ("pages/*.rst", "pages", "story.tmpl"), - ("pages/*.txt", "pages", "story.tmpl"), - ("pages/*.html", "pages", "story.tmpl"), + ("pages/*.rst", "pages", "page.tmpl"), + ("pages/*.md", "pages", "page.tmpl"), + ("pages/*.txt", "pages", "page.tmpl"), + ("pages/*.html", "pages", "page.tmpl"), )""", 'COMPILERS': """{ - "rest": ('.rst', '.txt'), - "markdown": ('.md', '.mdown', '.markdown'), - "textile": ('.textile',), - "txt2tags": ('.t2t',), - "bbcode": ('.bb',), - "wiki": ('.wiki',), - "ipynb": ('.ipynb',), - "html": ('.html', '.htm'), + "rest": ['.rst', '.txt'], + "markdown": ['.md', '.mdown', '.markdown'], + "textile": ['.textile'], + "txt2tags": ['.t2t'], + "bbcode": ['.bb'], + "wiki": ['.wiki'], + "ipynb": ['.ipynb'], + "html": ['.html', '.htm'], # PHP files are rendered the usual way (i.e. with the full templates). # The resulting files have .php extensions, making it possible to run # them without reconfiguring your server to recognize them. - "php": ('.php',), + "php": ['.php'], # Pandoc detects the input from the source filename # but is disabled by default as it would conflict # with many of the others. - # "pandoc": ('.rst', '.md', '.txt'), + # "pandoc": ['.rst', '.md', '.txt'], }""", 'NAVIGATION_LINKS': """{ DEFAULT_LANG: ( @@ -108,6 +109,7 @@ SAMPLE_CONF = { ), }""", 'REDIRECTIONS': [], + '_METADATA_MAPPING_FORMATS': ', '.join(LEGAL_VALUES['METADATA_MAPPING']) } @@ -171,6 +173,14 @@ def format_default_translations_config(additional_languages): return "{{\n{0}\n}}".format("\n".join(lang_paths)) +def get_default_translations_dict(default_lang, additional_languages): + """Generate a TRANSLATIONS dict matching the config from 'format_default_translations_config'.""" + tr = {default_lang: ''} + for l in additional_languages: + tr[l] = './' + l + return tr + + def format_navigation_links(additional_languages, default_lang, messages, strip_indexes=False): """Return the string to configure NAVIGATION_LINKS.""" f = u"""\ @@ -212,7 +222,7 @@ def prepare_config(config): """Parse sample config with JSON.""" p = config.copy() 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', 'FEED_READ_MORE_LINK')}) + if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'FEED_READ_MORE_LINK', '_METADATA_MAPPING_FORMATS')}) # READ_MORE_LINKs require some special treatment. p['INDEX_READ_MORE_LINK'] = "'" + p['INDEX_READ_MORE_LINK'].replace("'", "\\'") + "'" p['FEED_READ_MORE_LINK'] = "'" + p['FEED_READ_MORE_LINK'].replace("'", "\\'") + "'" @@ -285,7 +295,7 @@ class CommandInit(Command): @classmethod def create_empty_site(cls, target): """Create an empty site with directories only.""" - for folder in ('files', 'galleries', 'listings', 'posts', 'pages'): + for folder in ('files', 'galleries', 'images', 'listings', 'posts', 'pages'): makedirs(os.path.join(target, folder)) @staticmethod @@ -323,7 +333,6 @@ class CommandInit(Command): 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['STRIP_INDEXES'] = SAMPLE_CONF['PRETTY_URLS'] def lhandler(default, toconf, show_header=True): if show_header: @@ -354,9 +363,8 @@ class CommandInit(Command): # Get messages for navigation_links. In order to do this, we need # to generate a throwaway TRANSLATIONS dict. - tr = {default: ''} - for l in langs: - tr[l] = './' + l + tr = get_default_translations_dict(default, langs) + # Assuming that base contains all the locales, and that base does # not inherit from anywhere. try: @@ -377,22 +385,22 @@ class CommandInit(Command): while not answered: try: lz = get_localzone() - except: + except Exception: lz = None answer = ask('Time zone', lz if lz else "UTC") tz = dateutil.tz.gettz(answer) if tz is None: print(" WARNING: Time zone not found. Searching list of time zones for a match.") - zonesfile = tarfile.open(fileobj=dateutil.zoneinfo.getzoneinfofile_stream()) - zonenames = [zone for zone in zonesfile.getnames() if answer.lower() in zone.lower()] - if len(zonenames) == 1: - tz = dateutil.tz.gettz(zonenames[0]) - answer = zonenames[0] + all_zones = dateutil.zoneinfo.get_zonefile_instance().zones + matching_zones = [zone for zone in all_zones if answer.lower() in zone.lower()] + if len(matching_zones) == 1: + tz = dateutil.tz.gettz(matching_zones[0]) + answer = matching_zones[0] print(" Picking '{0}'.".format(answer)) - elif len(zonenames) > 1: + elif len(matching_zones) > 1: print(" The following time zones match your query:") - print(' ' + '\n '.join(zonenames)) + print(' ' + '\n '.join(matching_zones)) continue if tz is not None: diff --git a/nikola/plugins/command/install_theme.plugin b/nikola/plugins/command/install_theme.plugin deleted file mode 100644 index aa68773..0000000 --- a/nikola/plugins/command/install_theme.plugin +++ /dev/null @@ -1,13 +0,0 @@ -[Core] -name = install_theme -module = install_theme - -[Documentation] -author = Roberto Alsina -version = 1.0 -website = https://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 deleted file mode 100644 index 28f7aa3..0000000 --- a/nikola/plugins/command/install_theme.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2016 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Install a theme.""" - -from __future__ import print_function - -from nikola import utils -from nikola.plugin_categories import Command -LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER) - - -class CommandInstallTheme(Command): - """Install a theme.""" - - name = "install_theme" - doc_usage = "[[-u] theme_name] | [[-u] -l]" - doc_purpose = "install theme into current site" - output_dir = 'themes' - cmd_options = [ - { - 'name': 'list', - 'short': 'l', - 'long': 'list', - 'type': bool, - 'default': False, - 'help': 'Show list of available themes.' - }, - { - 'name': 'url', - 'short': 'u', - 'long': 'url', - 'type': str, - 'help': "URL for the theme repository (default: " - "https://themes.getnikola.com/v7/themes.json)", - 'default': 'https://themes.getnikola.com/v7/themes.json' - }, - { - 'name': 'getpath', - 'short': 'g', - 'long': 'get-path', - 'type': bool, - 'default': False, - 'help': "Print the path for installed theme", - }, - ] - - def _execute(self, options, args): - """Install theme into current site.""" - p = self.site.plugin_manager.getPluginByName('theme', 'Command').plugin_object - listing = options['list'] - url = options['url'] - if args: - name = args[0] - else: - name = None - - if options['getpath'] and name: - return p.get_path(name) - - if name is None and not listing: - LOGGER.error("This command needs either a theme name or the -l option.") - return False - - if listing: - p.list_available(url) - else: - p.do_install_deps(url, name) diff --git a/nikola/plugins/command/new_page.plugin b/nikola/plugins/command/new_page.plugin index 3eaecb4..8734805 100644 --- a/nikola/plugins/command/new_page.plugin +++ b/nikola/plugins/command/new_page.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create a new page. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/new_page.py b/nikola/plugins/command/new_page.py index c09b4be..0f7996a 100644 --- a/nikola/plugins/command/new_page.py +++ b/nikola/plugins/command/new_page.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others. +# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """Create a new page.""" -from __future__ import unicode_literals, print_function from nikola.plugin_categories import Command @@ -107,6 +106,7 @@ class CommandNewPage(Command): options['tags'] = '' options['schedule'] = False options['is_page'] = True + options['date-path'] = False # Even though stuff was split into `new_page`, it’s easier to do it # there not to duplicate the code. p = self.site.plugin_manager.getPluginByName('new_post', 'Command').plugin_object diff --git a/nikola/plugins/command/new_post.plugin b/nikola/plugins/command/new_post.plugin index e9c3af5..efdeb58 100644 --- a/nikola/plugins/command/new_post.plugin +++ b/nikola/plugins/command/new_post.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create a new post. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py index 36cc04f..e6eabbd 100644 --- a/nikola/plugins/command/new_post.py +++ b/nikola/plugins/command/new_post.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """Create a new post.""" -from __future__ import unicode_literals, print_function import io import datetime import operator @@ -35,15 +34,15 @@ import shutil import subprocess import sys -from blinker import signal import dateutil.tz +from blinker import signal from nikola.plugin_categories import Command from nikola import utils COMPILERS_DOC_LINK = 'https://getnikola.com/handbook.html#configuring-other-input-formats' -POSTLOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER) -PAGELOGGER = utils.get_logger('new_page', utils.STDERR_HANDLER) +POSTLOGGER = utils.get_logger('new_post') +PAGELOGGER = utils.get_logger('new_page') LOGGER = POSTLOGGER @@ -90,7 +89,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): except ImportError: LOGGER.error('To use the --schedule switch of new_post, ' 'you have to install the "dateutil" package.') - rrule = None # NOQA + rrule = None if schedule and rrule and rule: try: rule_ = rrule.rrulestr(rule, dtstart=last_date or date) @@ -111,7 +110,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): else: tz_str = ' UTC' - return date.strftime('%Y-%m-%d %H:%M:%S') + tz_str + return (date.strftime('%Y-%m-%d %H:%M:%S') + tz_str, date) class CommandNewPost(Command): @@ -204,7 +203,14 @@ class CommandNewPost(Command): 'default': '', 'help': 'Import an existing file instead of creating a placeholder' }, - + { + 'name': 'date-path', + 'short': 'd', + 'long': 'date-path', + 'type': bool, + 'default': False, + 'help': 'Create post with date path (eg. year/month/day, see NEW_POST_DATE_PATH_FORMAT in config)' + }, ] def _execute(self, options, args): @@ -234,6 +240,10 @@ class CommandNewPost(Command): twofile = options['twofile'] import_file = options['import'] wants_available = options['available-formats'] + date_path_opt = options['date-path'] + date_path_auto = self.site.config['NEW_POST_DATE_PATH'] and content_type == 'post' + date_path_format = self.site.config['NEW_POST_DATE_PATH_FORMAT'].strip('/') + post_type = options.get('type', 'text') if wants_available: self.print_compilers() @@ -255,16 +265,39 @@ class CommandNewPost(Command): if "@" in content_format: content_format, content_subformat = content_format.split("@") - if not content_format: # Issue #400 + if not content_format and path and not os.path.isdir(path): + # content_format not specified. If path was given, use + # it to guess (Issue #2798) + extension = os.path.splitext(path)[-1] + for compiler, extensions in self.site.config['COMPILERS'].items(): + if extension in extensions: + content_format = compiler + if not content_format: + LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension)) + return + + elif not content_format and import_file: + # content_format not specified. If import_file was given, use + # it to guess (Issue #2798) + extension = os.path.splitext(import_file)[-1] + for compiler, extensions in self.site.config['COMPILERS'].items(): + if extension in extensions: + content_format = compiler + if not content_format: + LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension)) + return + + elif not content_format: # Issue #400 content_format = get_default_compiler( is_post, self.site.config['COMPILERS'], self.site.config['post_pages']) - 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)) + elif content_format not in compiler_names: + LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, content_format)) self.print_compilers() return + compiler_plugin = self.site.plugin_manager.getPluginByName( content_format, "PageCompiler").plugin_object @@ -286,7 +319,7 @@ class CommandNewPost(Command): while not title: title = utils.ask('Title') - if isinstance(title, utils.bytes_str): + if isinstance(title, bytes): try: title = title.decode(sys.stdin.encoding) except (AttributeError, TypeError): # for tests @@ -296,26 +329,34 @@ class CommandNewPost(Command): if not path: slug = utils.slugify(title, lang=self.site.default_lang) else: - if isinstance(path, utils.bytes_str): + if isinstance(path, bytes): try: path = path.decode(sys.stdin.encoding) except (AttributeError, TypeError): # for tests path = path.decode('utf-8') - slug = utils.slugify(os.path.splitext(os.path.basename(path))[0], lang=self.site.default_lang) + if os.path.isdir(path): + # If the user provides a directory, add the file name generated from title (Issue #2651) + slug = utils.slugify(title, lang=self.site.default_lang) + pattern = os.path.basename(entry[0]) + suffix = pattern[1:] + path = os.path.join(path, slug + suffix) + else: + slug = utils.slugify(os.path.splitext(os.path.basename(path))[0], lang=self.site.default_lang) - if isinstance(author, utils.bytes_str): - try: - author = author.decode(sys.stdin.encoding) - except (AttributeError, TypeError): # for tests - author = author.decode('utf-8') + if isinstance(author, bytes): + try: + author = author.decode(sys.stdin.encoding) + except (AttributeError, TypeError): # for tests + author = author.decode('utf-8') # Calculate the date to use for the content - schedule = options['schedule'] or self.site.config['SCHEDULE_ALL'] + # SCHEDULE_ALL is post-only (Issue #2921) + schedule = options['schedule'] or (self.site.config['SCHEDULE_ALL'] and is_post) rule = self.site.config['SCHEDULE_RULE'] self.site.scan_posts() timeline = self.site.timeline last_date = None if not timeline else timeline[0].date - date = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601']) + date, dateobj = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601']) data = { 'title': title, 'slug': slug, @@ -323,17 +364,21 @@ class CommandNewPost(Command): 'tags': tags, 'link': '', 'description': '', - 'type': 'text', + 'type': post_type, } if not path: pattern = os.path.basename(entry[0]) suffix = pattern[1:] output_path = os.path.dirname(entry[0]) + if date_path_auto or date_path_opt: + output_path += os.sep + dateobj.strftime(date_path_format) txt_path = os.path.join(output_path, slug + suffix) meta_path = os.path.join(output_path, slug + ".meta") else: + if date_path_opt: + LOGGER.warning("A path has been specified, ignoring -d") txt_path = os.path.join(self.site.original_cwd, path) meta_path = os.path.splitext(txt_path)[0] + ".meta" @@ -360,18 +405,18 @@ class CommandNewPost(Command): metadata.update(self.site.config['ADDITIONAL_METADATA']) data.update(metadata) - # ipynb plugin needs the ipython kernel info. We get the kernel name + # ipynb plugin needs the Jupyter kernel info. We get the kernel name # from the content_subformat and pass it to the compiler in the metadata if content_format == "ipynb" and content_subformat is not None: - metadata["ipython_kernel"] = content_subformat + metadata["jupyter_kernel"] = content_subformat # Override onefile if not really supported. if not compiler_plugin.supports_onefile and onefile: onefile = False - LOGGER.warn('This compiler does not support one-file posts.') + LOGGER.warning('This compiler does not support one-file posts.') if onefile and import_file: - with io.open(import_file, 'r', encoding='utf-8') as fh: + with io.open(import_file, 'r', encoding='utf-8-sig') as fh: content = fh.read() elif not import_file: if is_page: @@ -385,13 +430,13 @@ class CommandNewPost(Command): else: compiler_plugin.create_post( txt_path, content=content, onefile=onefile, title=title, - slug=slug, date=date, tags=tags, is_page=is_page, **metadata) + slug=slug, date=date, tags=tags, is_page=is_page, type=post_type, **metadata) event = dict(path=txt_path) if not onefile: # write metadata file with io.open(meta_path, "w+", encoding="utf8") as fd: - fd.write(utils.write_metadata(data)) + fd.write(utils.write_metadata(data, comment_wrap=False, site=self.site)) LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path)) event['meta_path'] = meta_path LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path)) @@ -406,7 +451,7 @@ class CommandNewPost(Command): if editor: subprocess.call(to_run) else: - LOGGER.error('$EDITOR not set, cannot edit the post. Please do it manually.') + LOGGER.error('The $EDITOR environment variable is not set, cannot edit the post with \'-e\'. Please edit the post manually.') def filter_post_pages(self, compiler, is_post): """Return the correct entry from post_pages. @@ -523,6 +568,6 @@ class CommandNewPost(Command): 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 POSTS/PAGES tuples and any post scanners (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 d20c539..5107032 100644 --- a/nikola/plugins/command/orphans.plugin +++ b/nikola/plugins/command/orphans.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = List all orphans [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/orphans.py b/nikola/plugins/command/orphans.py index 5e2574d..0cf2e63 100644 --- a/nikola/plugins/command/orphans.py +++ b/nikola/plugins/command/orphans.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others. +# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """List all orphans.""" -from __future__ import print_function import os from nikola.plugin_categories import Command diff --git a/nikola/plugins/command/plugin.plugin b/nikola/plugins/command/plugin.plugin index 016bcaa..db99ceb 100644 --- a/nikola/plugins/command/plugin.plugin +++ b/nikola/plugins/command/plugin.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Manage Nikola plugins [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py index 364f343..33dee23 100644 --- a/nikola/plugins/command/plugin.py +++ b/nikola/plugins/command/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,8 +26,8 @@ """Manage plugins.""" -from __future__ import print_function import io +import json.decoder import os import sys import shutil @@ -42,7 +42,7 @@ from pygments.formatters import TerminalFormatter from nikola.plugin_categories import Command from nikola import utils -LOGGER = utils.get_logger('plugin', utils.STDERR_HANDLER) +LOGGER = utils.get_logger('plugin') class CommandPlugin(Command): @@ -84,9 +84,8 @@ class CommandPlugin(Command): 'short': 'u', 'long': 'url', 'type': str, - 'help': "URL for the plugin repository (default: " - "https://plugins.getnikola.com/v7/plugins.json)", - 'default': 'https://plugins.getnikola.com/v7/plugins.json' + 'help': "URL for the plugin repository", + 'default': 'https://plugins.getnikola.com/v8/plugins.json' }, { 'name': 'user', @@ -137,11 +136,11 @@ class CommandPlugin(Command): self.output_dir = options.get('output_dir') else: if not self.site.configured and not user_mode and install: - LOGGER.notice('No site found, assuming --user') + LOGGER.warning('No site found, assuming --user') user_mode = True if user_mode: - self.output_dir = os.path.expanduser('~/.nikola/plugins') + self.output_dir = os.path.expanduser(os.path.join('~', '.nikola', 'plugins')) else: self.output_dir = 'plugins' @@ -179,9 +178,18 @@ class CommandPlugin(Command): plugins.sort() print('Installed Plugins:') print('------------------') + maxlength = max(len(i[0]) for i in plugins) + if self.site.colorful: + formatstring = '\x1b[1m{0:<{2}}\x1b[0m at {1}' + else: + formatstring = '{0:<{2}} at {1}' for name, path in plugins: - print('{0} at {1}'.format(name, path)) - print('\n\nAlso, you have disabled these plugins: {}'.format(self.site.config['DISABLED_PLUGINS'])) + print(formatstring.format(name, path, maxlength)) + dp = self.site.config['DISABLED_PLUGINS'] + if dp: + print('\n\nAlso, you have disabled these plugins: {}'.format(', '.join(dp))) + else: + print('\n\nNo plugins are disabled.') return 0 def do_upgrade(self, url): @@ -235,43 +243,32 @@ class CommandPlugin(Command): utils.extract_all(zip_file, self.output_dir) dest_path = os.path.join(self.output_dir, name) else: - try: - plugin_path = utils.get_plugin_path(name) - except: - LOGGER.error("Can't find plugin " + name) - return 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 1 - - LOGGER.info('Copying {0} into plugins'.format(plugin_path)) - shutil.copytree(plugin_path, dest_path) + LOGGER.error("Can't find plugin " + name) + return 1 reqpath = os.path.join(dest_path, 'requirements.txt') if os.path.exists(reqpath): - LOGGER.notice('This plugin has Python dependencies.') + LOGGER.warning('This plugin has Python dependencies.') LOGGER.info('Installing dependencies with pip...') try: subprocess.check_call((sys.executable, '-m', 'pip', 'install', '-r', reqpath)) except subprocess.CalledProcessError: LOGGER.error('Could not install the dependencies.') print('Contents of the requirements.txt file:\n') - with io.open(reqpath, 'r', encoding='utf-8') as fh: + with io.open(reqpath, 'r', encoding='utf-8-sig') as fh: print(utils.indent(fh.read(), 4 * ' ')) print('You have to install those yourself or through a ' 'package manager.') else: LOGGER.info('Dependency installation succeeded.') + reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt') if os.path.exists(reqnpypath): - LOGGER.notice('This plugin has third-party ' - 'dependencies you need to install ' - 'manually.') + LOGGER.warning('This plugin has third-party ' + 'dependencies you need to install ' + 'manually.') print('Contents of the requirements-nonpy.txt file:\n') - with io.open(reqnpypath, 'r', encoding='utf-8') as fh: + with io.open(reqnpypath, 'r', encoding='utf-8-sig') as fh: for l in fh.readlines(): i, j = l.split('::') print(utils.indent(i.strip(), 4 * ' ')) @@ -280,17 +277,36 @@ class CommandPlugin(Command): print('You have to install those yourself or through a package ' 'manager.') + + req_plug_path = os.path.join(dest_path, 'requirements-plugins.txt') + if os.path.exists(req_plug_path): + LOGGER.info('This plugin requires other Nikola plugins.') + LOGGER.info('Installing plugins...') + plugin_failure = False + try: + with io.open(req_plug_path, 'r', encoding='utf-8-sig') as inf: + for plugname in inf.readlines(): + plugin_failure = self.do_install(url, plugname.strip(), show_install_notes) != 0 + except Exception: + plugin_failure = True + if plugin_failure: + LOGGER.error('Could not install a plugin.') + print('Contents of the requirements-plugins.txt file:\n') + with io.open(req_plug_path, 'r', encoding='utf-8-sig') as fh: + print(utils.indent(fh.read(), 4 * ' ')) + print('You have to install those yourself manually.') + else: + LOGGER.info('Dependency installation succeeded.') + confpypath = os.path.join(dest_path, 'conf.py.sample') 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!') + LOGGER.warning('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: + with io.open(confpypath, 'r', encoding='utf-8-sig') as fh: if self.site.colorful: - print(utils.indent(pygments.highlight( - fh.read(), PythonLexer(), TerminalFormatter()), - 4 * ' ')) + print(pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter())) else: - print(utils.indent(fh.read(), 4 * ' ')) + print(fh.read()) return 0 def do_uninstall(self, name): @@ -320,10 +336,19 @@ class CommandPlugin(Command): """Download the JSON file with all plugins.""" if self.json is None: 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() + 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() + except json.decoder.JSONDecodeError as e: + LOGGER.error("Failed to decode JSON data in response from server.") + LOGGER.error("JSON error encountered: " + str(e)) + LOGGER.error("This issue might be caused by server-side issues, or by to unusual activity in your " + "network (as determined by CloudFlare). Please visit https://plugins.getnikola.com/ in " + "a browser.") + sys.exit(2) + return self.json diff --git a/nikola/plugins/command/rst2html.plugin b/nikola/plugins/command/rst2html.plugin index a095705..6f2fb25 100644 --- a/nikola/plugins/command/rst2html.plugin +++ b/nikola/plugins/command/rst2html.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Compile reStructuredText to HTML using the Nikola architecture [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/rst2html/__init__.py b/nikola/plugins/command/rst2html/__init__.py index c877f63..5576b35 100644 --- a/nikola/plugins/command/rst2html/__init__.py +++ b/nikola/plugins/command/rst2html/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2015-2016 Chris Warrick and others. +# Copyright © 2015-2020 Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """Compile reStructuredText to HTML, using Nikola architecture.""" -from __future__ import unicode_literals, print_function import io import lxml.html @@ -50,12 +49,12 @@ class CommandRst2Html(Command): print("This command takes only one argument (input file name).") return 2 source = args[0] - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - output, error_level, deps = compiler.compile_html_string(data, source, True) + output, error_level, deps, shortcode_deps = compiler.compile_string(data, source, True) - rstcss_path = resource_filename('nikola', 'data/themes/base/assets/css/rst.css') - with io.open(rstcss_path, "r", encoding="utf8") as fh: + rstcss_path = resource_filename('nikola', 'data/themes/base/assets/css/rst_base.css') + with io.open(rstcss_path, "r", encoding="utf-8-sig") as fh: rstcss = fh.read() template_path = resource_filename('nikola', 'plugins/command/rst2html/rst2html.tmpl') diff --git a/nikola/plugins/command/serve.plugin b/nikola/plugins/command/serve.plugin index a4a726f..aa40073 100644 --- a/nikola/plugins/command/serve.plugin +++ b/nikola/plugins/command/serve.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Start test server. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index c9702d5..ede5179 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,26 +26,18 @@ """Start test server.""" -from __future__ import print_function import os +import sys import re +import signal import socket import webbrowser -try: - from BaseHTTPServer import HTTPServer - from SimpleHTTPServer import SimpleHTTPRequestHandler -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 http.server import HTTPServer +from http.server import SimpleHTTPRequestHandler +from io import BytesIO as StringIO from nikola.plugin_categories import Command -from nikola.utils import dns_sd, get_logger, STDERR_HANDLER +from nikola.utils import dns_sd class IPv6Server(HTTPServer): @@ -60,7 +52,6 @@ class CommandServe(Command): name = "serve" doc_usage = "[options]" doc_purpose = "start the test webserver" - logger = None dns_sd = None cmd_options = ( @@ -70,7 +61,7 @@ class CommandServe(Command): 'long': 'port', 'default': 8000, 'type': int, - 'help': 'Port number (default: 8000)', + 'help': 'Port number', }, { 'name': 'address', @@ -78,7 +69,7 @@ class CommandServe(Command): 'long': 'address', 'type': str, 'default': '', - 'help': 'Address to bind (default: 0.0.0.0 -- all local IPv4 interfaces)', + 'help': 'Address to bind, defaults to all local IPv4 interfaces', }, { 'name': 'detach', @@ -106,13 +97,24 @@ class CommandServe(Command): }, ) + def shutdown(self, signum=None, _frame=None): + """Shut down the server that is running detached.""" + if self.dns_sd: + self.dns_sd.Reset() + if os.path.exists(self.serve_pidfile): + os.remove(self.serve_pidfile) + if not self.detached: + self.logger.info("Server is shutting down.") + if signum: + sys.exit(0) + def _execute(self, options, args): """Start test server.""" - 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)) else: + self.serve_pidfile = os.path.abspath('nikolaserve.pid') os.chdir(out_dir) if '[' in options['address']: options['address'] = options['address'].strip('[').strip(']') @@ -128,35 +130,43 @@ class CommandServe(Command): httpd = OurHTTP((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() - self.logger.info("Serving HTTP on {0} port {1}...".format(*sa)) + if ipv6: + server_url = "http://[{0}]:{1}/".format(*sa) + else: + server_url = "http://{0}:{1}/".format(*sa) + self.logger.info("Serving on {0} ...".format(server_url)) + if options['browser']: - if ipv6: - server_url = "http://[{0}]:{1}/".format(*sa) - else: - server_url = "http://{0}:{1}/".format(*sa) + # Some browsers fail to load 0.0.0.0 (Issue #2755) + if sa[0] == '0.0.0.0': + server_url = "http://127.0.0.1:{1}/".format(*sa) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open(server_url) if options['detach']: + self.detached = True OurHTTPRequestHandler.quiet = True try: pid = os.fork() if pid == 0: + signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() else: - self.logger.info("Detached with PID {0}. Run `kill {0}` to stop the server.".format(pid)) - except AttributeError as e: + with open(self.serve_pidfile, 'w') as fh: + fh.write('{0}\n'.format(pid)) + self.logger.info("Detached with PID {0}. Run `kill {0}` or `kill $(cat nikolaserve.pid)` to stop the server.".format(pid)) + except AttributeError: if os.name == 'nt': self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") else: - raise e + raise else: + self.detached = False try: self.dns_sd = dns_sd(options['port'], (options['ipv6'] or '::' in options['address'])) + signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() except KeyboardInterrupt: - self.logger.info("Server is shutting down.") - if self.dns_sd: - self.dns_sd.Reset() + self.shutdown() return 130 @@ -172,8 +182,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): if self.quiet: return else: - # Old-style class in Python 2.7, cannot use super() - return SimpleHTTPRequestHandler.log_message(self, *args) + return super().log_message(*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 @@ -185,9 +194,9 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): # Note that it might break in future versions of Python, in which case we # would need to do even more magic. def send_head(self): - """Common code for GET and HEAD commands. + """Send response code and MIME header. - This sends the response code and MIME headers. + This is common code for GET and HEAD commands. Return value is either a file object (which has to be copied to the outputfile by the caller unless the command was HEAD, @@ -198,10 +207,12 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): path = self.translate_path(self.path) f = None if os.path.isdir(path): - if not self.path.endswith('/'): + path_parts = list(self.path.partition('?')) + if not path_parts[0].endswith('/'): # redirect browser - doing basically what apache does + path_parts[0] += '/' self.send_response(301) - self.send_header("Location", self.path + "/") + self.send_header("Location", ''.join(path_parts)) # begin no-cache patch # For redirects. With redirects, caching is even worse and can # break more. Especially with 301 Moved Permanently redirects, @@ -227,7 +238,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): # transmitted *less* than the content-length! f = open(path, 'rb') except IOError: - self.send_error(404, "File not found") + self.send_error(404, "File not found: {}".format(path)) return None filtered_bytes = None @@ -235,7 +246,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler): # 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 = re.sub(r'<base\s([^>]*)>', r'<!--base \g<1>-->', data, flags=re.IGNORECASE) data = data.encode('utf8') f = StringIO() f.write(data) diff --git a/nikola/plugins/command/status.plugin b/nikola/plugins/command/status.plugin index 91390d2..7e2bd96 100644 --- a/nikola/plugins/command/status.plugin +++ b/nikola/plugins/command/status.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com description = Site status [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/status.py b/nikola/plugins/command/status.py index b3ffbb4..c96d13f 100644 --- a/nikola/plugins/command/status.py +++ b/nikola/plugins/command/status.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """Display site status.""" -from __future__ import print_function import os from datetime import datetime from dateutil.tz import gettz, tzlocal diff --git a/nikola/plugins/command/bootswatch_theme.plugin b/nikola/plugins/command/subtheme.plugin index 51e6718..d377e22 100644 --- a/nikola/plugins/command/bootswatch_theme.plugin +++ b/nikola/plugins/command/subtheme.plugin @@ -1,13 +1,13 @@ [Core] -name = bootswatch_theme -module = bootswatch_theme +name = subtheme +module = subtheme [Documentation] author = Roberto Alsina -version = 1.0 +version = 1.1 website = https://getnikola.com/ -description = Given a swatch name and a parent theme, creates a custom theme. +description = Given a swatch name and a parent theme, creates a custom subtheme. [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/subtheme.py b/nikola/plugins/command/subtheme.py new file mode 100644 index 0000000..554a241 --- /dev/null +++ b/nikola/plugins/command/subtheme.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Given a swatch name from bootswatch.com or hackerthemes.com and a parent theme, creates a custom theme.""" + +import configparser +import os + +import requests + +from nikola import utils +from nikola.plugin_categories import Command + +LOGGER = utils.get_logger('subtheme') + + +def _check_for_theme(theme, themes): + for t in themes: + if t.endswith(os.sep + theme): + return True + return False + + +class CommandSubTheme(Command): + """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" + + name = "subtheme" + doc_usage = "[options]" + doc_purpose = "given a swatch name from bootswatch.com or hackerthemes.com and a parent theme, creates a custom"\ + " theme" + cmd_options = [ + { + 'name': 'name', + 'short': 'n', + 'long': 'name', + 'default': 'custom', + 'type': str, + 'help': 'New theme name', + }, + { + 'name': 'swatch', + 'short': 's', + 'default': '', + 'type': str, + 'help': 'Name of the swatch from bootswatch.com.' + }, + { + 'name': 'parent', + 'short': 'p', + 'long': 'parent', + 'default': 'bootstrap4', + 'help': 'Parent theme name', + }, + ] + + def _execute(self, options, args): + """Given a swatch name and a parent theme, creates a custom theme.""" + name = options['name'] + swatch = options['swatch'] + if not swatch: + LOGGER.error('The -s option is mandatory') + return 1 + parent = options['parent'] + version = '4' + + # Check which Bootstrap version to use + themes = utils.get_theme_chain(parent, self.site.themes_dirs) + if _check_for_theme('bootstrap', themes) or _check_for_theme('bootstrap-jinja', themes): + version = '2' + elif _check_for_theme('bootstrap3', themes) or _check_for_theme('bootstrap3-jinja', themes): + version = '3' + elif _check_for_theme('bootstrap4', themes) or _check_for_theme('bootstrap4-jinja', themes): + version = '4' + elif not _check_for_theme('bootstrap4', themes) and not _check_for_theme('bootstrap4-jinja', themes): + LOGGER.warning( + '"subtheme" only makes sense for themes that use bootstrap') + elif _check_for_theme('bootstrap3-gradients', themes) or _check_for_theme('bootstrap3-gradients-jinja', themes): + LOGGER.warning( + '"subtheme" doesn\'t work well with the bootstrap3-gradients family') + + LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format( + name, swatch, parent)) + utils.makedirs(os.path.join('themes', name, 'assets', 'css')) + for fname in ('bootstrap.min.css', 'bootstrap.css'): + if swatch in [ + 'bubblegum', 'business-tycoon', 'charming', 'daydream', + 'executive-suite', 'good-news', 'growth', 'harbor', 'hello-world', + 'neon-glow', 'pleasant', 'retro', 'vibrant-sea', 'wizardry']: # Hackerthemes + LOGGER.info( + 'Hackertheme-based subthemes often require you use a custom font for full effect.') + if version != '4': + LOGGER.error( + 'The hackertheme subthemes are only available for Bootstrap 4.') + return 1 + if fname == 'bootstrap.css': + url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.css'.format( + swatch=swatch) + else: + url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.min.css'.format( + swatch=swatch) + else: # Bootswatch + url = 'https://bootswatch.com' + if version: + url += '/' + version + url = '/'.join((url, swatch, fname)) + LOGGER.info("Downloading: " + url) + r = requests.get(url) + if r.status_code > 299: + LOGGER.error('Error {} getting {}', r.status_code, url) + return 1 + data = r.text + + with open(os.path.join('themes', name, 'assets', 'css', fname), + 'w+') as output: + output.write(data) + + with open(os.path.join('themes', name, '%s.theme' % name), 'w+') as output: + parent_theme_data_path = utils.get_asset_path( + '%s.theme' % parent, themes) + cp = configparser.ConfigParser() + cp.read(parent_theme_data_path) + cp['Theme']['parent'] = parent + cp['Family'] = {'family': cp['Family']['family']} + cp.write(output) + + LOGGER.info( + 'Theme created. Change the THEME setting to "{0}" to use it.'.format(name)) diff --git a/nikola/plugins/command/theme.plugin b/nikola/plugins/command/theme.plugin index b0c1886..421d027 100644 --- a/nikola/plugins/command/theme.plugin +++ b/nikola/plugins/command/theme.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Manage Nikola themes [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/theme.py b/nikola/plugins/command/theme.py index 7513491..6f4339a 100644 --- a/nikola/plugins/command/theme.py +++ b/nikola/plugins/command/theme.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others. +# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,13 +26,15 @@ """Manage themes.""" -from __future__ import print_function -import os +import configparser import io +import json.decoder +import os import shutil +import sys import time -import requests +import requests import pygments from pygments.lexers import PythonLexer from pygments.formatters import TerminalFormatter @@ -41,7 +43,7 @@ from pkg_resources import resource_filename from nikola.plugin_categories import Command from nikola import utils -LOGGER = utils.get_logger('theme', utils.STDERR_HANDLER) +LOGGER = utils.get_logger('theme') class CommandTheme(Command): @@ -89,9 +91,8 @@ class CommandTheme(Command): 'short': 'u', 'long': 'url', 'type': str, - 'help': "URL for the theme repository (default: " - "https://themes.getnikola.com/v7/themes.json)", - 'default': 'https://themes.getnikola.com/v7/themes.json' + 'help': "URL for the theme repository", + 'default': 'https://themes.getnikola.com/v8/themes.json' }, { 'name': 'getpath', @@ -122,14 +123,21 @@ class CommandTheme(Command): 'long': 'engine', 'type': str, 'default': 'mako', - 'help': 'Engine to use for new theme (mako or jinja -- default: mako)', + 'help': 'Engine to use for new theme (mako or jinja)', }, { 'name': 'new_parent', 'long': 'parent', 'type': str, 'default': 'base', - 'help': 'Parent to use for new theme (default: base)', + 'help': 'Parent to use for new theme', + }, + { + 'name': 'new_legacy_meta', + 'long': 'legacy-meta', + 'type': bool, + 'default': False, + 'help': 'Create legacy meta files for new theme', }, ] @@ -147,6 +155,7 @@ class CommandTheme(Command): new = options.get('new') new_engine = options.get('new_engine') new_parent = options.get('new_parent') + new_legacy_meta = options.get('new_legacy_meta') command_count = [bool(x) for x in ( install, uninstall, @@ -172,7 +181,7 @@ class CommandTheme(Command): elif copy_template: return self.copy_template(copy_template) elif new: - return self.new_theme(new, new_engine, new_parent) + return self.new_theme(new, new_engine, new_parent, new_legacy_meta) def do_install_deps(self, url, name): """Install themes and their dependencies.""" @@ -188,11 +197,11 @@ class CommandTheme(Command): try: utils.get_theme_path_real(parent_name, self.site.themes_dirs) break - except: # Not available + except Exception: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: - LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname)) + LOGGER.info('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.""" @@ -225,15 +234,13 @@ class CommandTheme(Command): confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): - LOGGER.notice('This theme has a sample config file. Integrate it with yours in order to make this theme work!') + LOGGER.warning('This theme has a sample config file. Integrate it with yours in order to make this theme work!') print('Contents of the conf.py.sample file:\n') - with io.open(confpypath, 'r', encoding='utf-8') as fh: + with io.open(confpypath, 'r', encoding='utf-8-sig') as fh: if self.site.colorful: - print(utils.indent(pygments.highlight( - fh.read(), PythonLexer(), TerminalFormatter()), - 4 * ' ')) + print(pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter())) else: - print(utils.indent(fh.read(), 4 * ' ')) + print(fh.read()) return True def do_uninstall(self, name): @@ -282,7 +289,9 @@ class CommandTheme(Command): themes = [] themes_dirs = self.site.themes_dirs + [resource_filename('nikola', os.path.join('data', 'themes'))] for tdir in themes_dirs: - themes += [(i, os.path.join(tdir, i)) for i in os.listdir(tdir)] + if os.path.isdir(tdir): + themes += [(i, os.path.join(tdir, i)) for i in os.listdir(tdir)] + for tname, tpath in sorted(set(themes)): if os.path.isdir(tpath): print("{0} at {1}".format(tname, tpath)) @@ -316,7 +325,7 @@ class CommandTheme(Command): LOGGER.error("This file already exists in your templates directory ({0}).".format(base)) return 3 - def new_theme(self, name, engine, parent): + def new_theme(self, name, engine, parent, create_legacy_meta=False): """Create a new theme.""" base = 'themes' themedir = os.path.join(base, name) @@ -326,9 +335,7 @@ class CommandTheme(Command): LOGGER.info("Created directory {0}".format(base)) # Check if engine and parent match - engine_file = utils.get_asset_path('engine', utils.get_theme_chain(parent, self.site.themes_dirs)) - with io.open(engine_file, 'r', encoding='utf-8') as fh: - parent_engine = fh.read().strip() + parent_engine = utils.get_template_engine(utils.get_theme_chain(parent, self.site.themes_dirs)) if parent_engine != engine: LOGGER.error("Cannot use engine {0} because parent theme '{1}' uses {2}".format(engine, parent, parent_engine)) @@ -342,24 +349,45 @@ class CommandTheme(Command): LOGGER.error("Theme already exists") return 2 - with io.open(os.path.join(themedir, 'parent'), 'w', encoding='utf-8') as fh: - fh.write(parent + '\n') - LOGGER.info("Created file {0}".format(os.path.join(themedir, 'parent'))) - with io.open(os.path.join(themedir, 'engine'), 'w', encoding='utf-8') as fh: - fh.write(engine + '\n') - LOGGER.info("Created file {0}".format(os.path.join(themedir, 'engine'))) + cp = configparser.ConfigParser() + cp['Theme'] = { + 'engine': engine, + 'parent': parent + } + + theme_meta_path = os.path.join(themedir, name + '.theme') + with io.open(theme_meta_path, 'w', encoding='utf-8') as fh: + cp.write(fh) + LOGGER.info("Created file {0}".format(theme_meta_path)) + + if create_legacy_meta: + with io.open(os.path.join(themedir, 'parent'), 'w', encoding='utf-8') as fh: + fh.write(parent + '\n') + LOGGER.info("Created file {0}".format(os.path.join(themedir, 'parent'))) + with io.open(os.path.join(themedir, 'engine'), 'w', encoding='utf-8') as fh: + fh.write(engine + '\n') + LOGGER.info("Created file {0}".format(os.path.join(themedir, 'engine'))) LOGGER.info("Theme {0} created successfully.".format(themedir)) - LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(name)) + LOGGER.info('Remember to set THEME="{0}" in conf.py to use this theme.'.format(name)) def get_json(self, url): """Download the JSON file with all plugins.""" if self.json is None: 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() + 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() + except json.decoder.JSONDecodeError as e: + LOGGER.error("Failed to decode JSON data in response from server.") + LOGGER.error("JSON error encountered:" + str(e)) + LOGGER.error("This issue might be caused by server-side issues, or by to unusual activity in your " + "network (as determined by CloudFlare). Please visit https://themes.getnikola.com/ in " + "a browser.") + sys.exit(2) + return self.json diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin index d78b79b..a172e28 100644 --- a/nikola/plugins/command/version.plugin +++ b/nikola/plugins/command/version.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Show nikola version [Nikola] -plugincategory = Command +PluginCategory = Command diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py index 267837e..9b81343 100644 --- a/nikola/plugins/command/version.py +++ b/nikola/plugins/command/version.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,13 @@ """Print Nikola version.""" -from __future__ import print_function -import lxml import requests from nikola.plugin_categories import Command from nikola import __version__ -URL = 'https://pypi.python.org/pypi?:action=doap&name=Nikola' +URL = 'https://pypi.org/pypi/Nikola/json' class CommandVersion(Command): @@ -60,10 +58,11 @@ class CommandVersion(Command): """Print the version number.""" print("Nikola v" + __version__) if options.get('check'): - data = requests.get(URL).text - doc = lxml.etree.fromstring(data.encode('utf8')) - revision = doc.findall('*//{http://usefulinc.com/ns/doap#}revision')[0].text - if revision == __version__: + data = requests.get(URL).json() + pypi_version = data['info']['version'] + if pypi_version == __version__: print("Nikola is up-to-date") else: - print("The latest version of Nikola is v{0} -- please upgrade using `pip install --upgrade Nikola=={0}` or your system package manager".format(revision)) + print("The latest version of Nikola is v{0}. Please upgrade " + "using `pip install --upgrade Nikola=={0}` or your " + "system package manager.".format(pypi_version)) diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py index ff7e9a2..db78fce 100644 --- a/nikola/plugins/compile/__init__.py +++ b/nikola/plugins/compile/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/compile/html.plugin b/nikola/plugins/compile/html.plugin index f95bdd5..be1f876 100644 --- a/nikola/plugins/compile/html.plugin +++ b/nikola/plugins/compile/html.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Compile HTML into HTML (just copy) [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = HTML diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py index 942d6da..80b6713 100644 --- a/nikola/plugins/compile/html.py +++ b/nikola/plugins/compile/html.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,15 +24,17 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html for HTML source files.""" +"""Page compiler plugin for HTML source files.""" -from __future__ import unicode_literals -import os import io +import os + +import lxml.html +from nikola import shortcodes as sc from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, write_metadata +from nikola.utils import LocaleBorg, makedirs, map_metadata, write_metadata class CompileHtml(PageCompiler): @@ -40,25 +42,27 @@ class CompileHtml(PageCompiler): name = "html" friendly_name = "HTML" + supports_metadata = True - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile HTML into HTML strings, with shortcode support.""" + if not is_two_file: + _, data = self.split_metadata(data, post, lang) + new_data, shortcodes = sc.extract_shortcodes(data) + return self.site.apply_shortcodes_uuid(new_data, shortcodes, filename=source_path, extra_context={'post': post}) + + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) - try: - post = self.site.post_per_input_file[source] - except KeyError: - post = None - with io.open(dest, "w+", encoding="utf8") as out_file: - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - if not is_two_file: - _, data = self.split_metadata(data) - data, shortcode_deps = self.site.apply_shortcodes(data, with_dependencies=True, extra_context=dict(post=post)) + data, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(data) if post is None: if shortcode_deps: self.logger.error( - "Cannot save dependencies for post {0} due to unregistered source file name", + "Cannot save dependencies for post {0} (post unknown)", source) else: post._depfile[dest] += shortcode_deps @@ -76,9 +80,41 @@ class CompileHtml(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) + + def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): + """Read the metadata from a post's meta tags, and return a metadata dict.""" + if lang is None: + lang = LocaleBorg().current_lang + source_path = post.translated_source_path(lang) + + with io.open(source_path, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + + metadata = {} + try: + doc = lxml.html.document_fromstring(data) + except lxml.etree.ParserError as e: + # Issue #374 -> #2851 + if str(e) == "Document is empty": + return {} + # let other errors raise + raise + title_tag = doc.find('*//title') + if title_tag is not None and title_tag.text: + metadata['title'] = title_tag.text + meta_tags = doc.findall('*//meta') + for tag in meta_tags: + k = tag.get('name', '').lower() + if not k: + continue + elif k == 'keywords': + k = 'tags' + content = tag.get('content') + if content: + metadata[k] = content + map_metadata(metadata, 'html_metadata', self.site.config) + return metadata diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin index c369ab2..c146172 100644 --- a/nikola/plugins/compile/ipynb.plugin +++ b/nikola/plugins/compile/ipynb.plugin @@ -6,8 +6,8 @@ module = ipynb author = Damian Avila, Chris Warrick and others version = 2.0.0 website = http://www.damian.oquanta.info/ -description = Compile IPython notebooks into Nikola posts +description = Compile Jupyter notebooks into Nikola posts [Nikola] -plugincategory = Compiler -friendlyname = Jupyter/IPython Notebook +PluginCategory = Compiler +friendlyname = Jupyter Notebook diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py index f3fdeea..039604b 100644 --- a/nikola/plugins/compile/ipynb.py +++ b/nikola/plugins/compile/ipynb.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2013-2016 Damián Avila, Chris Warrick and others. +# Copyright © 2013-2020 Damián Avila, Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,99 +24,95 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on nbconvert.""" +"""Page compiler plugin for nbconvert.""" -from __future__ import unicode_literals, print_function import io +import json import os -import sys try: + import nbconvert from nbconvert.exporters import HTMLExporter import nbformat current_nbformat = nbformat.current_nbformat from jupyter_client import kernelspec from traitlets.config import Config + NBCONVERT_VERSION_MAJOR = int(nbconvert.__version__.partition(".")[0]) flag = True - ipy_modern = True except ImportError: - try: - import IPython - from IPython.nbconvert.exporters import HTMLExporter - if IPython.version_info[0] >= 3: # API changed with 3.0.0 - from IPython import nbformat - current_nbformat = nbformat.current_nbformat - from IPython.kernel import kernelspec - ipy_modern = True - else: - import IPython.nbformat.current as nbformat - current_nbformat = 'json' - kernelspec = None - ipy_modern = False - - from IPython.config import Config - flag = True - except ImportError: - flag = None - ipy_modern = None + flag = None +from nikola import shortcodes as sc from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing, get_logger, STDERR_HANDLER +from nikola.utils import makedirs, req_missing, LocaleBorg class CompileIPynb(PageCompiler): """Compile IPynb into HTML.""" name = "ipynb" - friendly_name = "Jupyter/IPython Notebook" + friendly_name = "Jupyter Notebook" demote_headers = True - default_kernel = 'python2' if sys.version_info[0] == 2 else 'python3' + default_kernel = 'python3' + supports_metadata = True - def set_site(self, site): - """Set Nikola site.""" - self.logger = get_logger('compile_ipynb', STDERR_HANDLER) - super(CompileIPynb, self).set_site(site) - - def compile_html_string(self, source, is_two_file=True): + def _compile_string(self, nb_json): """Export notebooks as HTML strings.""" - if flag is None: - req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') - c = Config(self.site.config['IPYNB_CONFIG']) + self._req_missing_ipynb() + c = Config(get_default_jupyter_config()) + c.merge(Config(self.site.config['IPYNB_CONFIG'])) + if 'template_file' not in self.site.config['IPYNB_CONFIG'].get('Exporter', {}): + if NBCONVERT_VERSION_MAJOR >= 6: + c['Exporter']['template_file'] = 'classic/base.html.j2' + else: + c['Exporter']['template_file'] = 'basic.tpl' # not a typo 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) + body, _ = 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.""" + @staticmethod + def _nbformat_read(in_file): + return nbformat.read(in_file, current_nbformat) + + def _req_missing_ipynb(self): + if flag is None: + req_missing(['notebook>=4.0.0'], 'build this site (compile ipynb)') + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile notebooks into HTML strings.""" + new_data, shortcodes = sc.extract_shortcodes(data) + output = self._compile_string(nbformat.reads(new_data, current_nbformat)) + return self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + + def compile(self, source, dest, is_two_file=False, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) - try: - post = self.site.post_per_input_file[source] - except KeyError: - post = None - with io.open(dest, "w+", encoding="utf8") as out_file: - output = self.compile_html_string(source, is_two_file) - output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post)) + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: + nb_str = in_file.read() + output, shortcode_deps = self.compile_string(nb_str, source, + is_two_file, post, + lang) out_file.write(output) if post is None: if shortcode_deps: self.logger.error( - "Cannot save dependencies for post {0} due to unregistered source file name", + "Cannot save dependencies for post {0} (post unknown)", source) else: post._depfile[dest] += shortcode_deps - def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): + def read_metadata(self, post, lang=None): """Read metadata directly from ipynb file. - As ipynb file support arbitrary metadata as json, the metadata used by Nikola + As ipynb files support arbitrary metadata as json, the metadata used by Nikola will be assume to be in the 'nikola' subfield. """ - if flag is None: - req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') - source = post.source_path - with io.open(source, "r", encoding="utf8") as in_file: + self._req_missing_ipynb() + if lang is None: + lang = LocaleBorg().current_lang + source = post.translated_source_path(lang) + with io.open(source, "r", encoding="utf-8-sig") as in_file: nb_json = nbformat.read(in_file, current_nbformat) # Metadata might not exist in two-file posts or in hand-crafted # .ipynb files. @@ -124,11 +120,10 @@ class CompileIPynb(PageCompiler): 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)') + self._req_missing_ipynb() content = kw.pop('content', None) onefile = kw.pop('onefile', False) - kernel = kw.pop('ipython_kernel', None) + kernel = kw.pop('jupyter_kernel', None) # is_page is not needed to create the file kw.pop('is_page', False) @@ -142,40 +137,52 @@ class CompileIPynb(PageCompiler): # imported .ipynb file, guaranteed to start with "{" because it’s JSON. nb = nbformat.reads(content, current_nbformat) else: - if ipy_modern: - nb = nbformat.v4.new_notebook() - nb["cells"] = [nbformat.v4.new_markdown_cell(content)] - else: - nb = nbformat.new_notebook() - nb["worksheets"] = [nbformat.new_worksheet(cells=[nbformat.new_text_cell('markdown', [content])])] - - if kernelspec is not None: - if kernel is None: - kernel = self.default_kernel - self.logger.notice('No kernel specified, assuming "{0}".'.format(kernel)) - - IPYNB_KERNELS = {} - ksm = kernelspec.KernelSpecManager() - for k in ksm.find_kernel_specs(): - IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict() - IPYNB_KERNELS[k]['name'] = k - del IPYNB_KERNELS[k]['argv'] - - if kernel not in IPYNB_KERNELS: - self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel)) - self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS)))) - raise Exception('Unknown kernel "{0}"'.format(kernel)) - - nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel] - else: - # Older IPython versions don’t need kernelspecs. - pass + nb = nbformat.v4.new_notebook() + nb["cells"] = [nbformat.v4.new_markdown_cell(content)] + + if kernel is None: + kernel = self.default_kernel + self.logger.warning('No kernel specified, assuming "{0}".'.format(kernel)) + + IPYNB_KERNELS = {} + ksm = kernelspec.KernelSpecManager() + for k in ksm.find_kernel_specs(): + IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict() + IPYNB_KERNELS[k]['name'] = k + del IPYNB_KERNELS[k]['argv'] + + if kernel not in IPYNB_KERNELS: + self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel)) + self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS)))) + raise Exception('Unknown kernel "{0}"'.format(kernel)) + + nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel] if onefile: nb["metadata"]["nikola"] = metadata - with io.open(path, "w+", encoding="utf8") as fd: - if ipy_modern: - nbformat.write(nb, fd, 4) - else: - nbformat.write(nb, fd, 'ipynb') + with io.open(path, "w+", encoding="utf-8") as fd: + nbformat.write(nb, fd, 4) + + +def get_default_jupyter_config(): + """Search default jupyter configuration location paths. + + Return dictionary from configuration json files. + """ + config = {} + from jupyter_core.paths import jupyter_config_path + + for parent in jupyter_config_path(): + try: + for file in os.listdir(parent): + if 'nbconvert' in file and file.endswith('.json'): + abs_path = os.path.join(parent, file) + with open(abs_path) as config_file: + config.update(json.load(config_file)) + except OSError: + # some paths jupyter uses to find configurations + # may not exist + pass + + return config diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin index 2607413..85c67c3 100644 --- a/nikola/plugins/compile/markdown.plugin +++ b/nikola/plugins/compile/markdown.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Compile Markdown into HTML [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = Markdown diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index 2e4234c..74e8c75 100644 --- a/nikola/plugins/compile/markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,23 +24,44 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on markdown.""" - -from __future__ import unicode_literals +"""Page compiler plugin for Markdown.""" import io +import json import os +import threading + +from nikola import shortcodes as sc +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing, write_metadata, LocaleBorg, map_metadata try: - from markdown import markdown + from markdown import Markdown except ImportError: - markdown = None # NOQA - nikola_extension = None - gist_extension = None - podcast_extension = None + Markdown = None -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing, write_metadata + +class ThreadLocalMarkdown(threading.local): + """Convert Markdown to HTML using per-thread Markdown objects. + + See discussion in #2661. + """ + + def __init__(self, extensions, extension_configs): + """Create a Markdown instance.""" + self.markdown = Markdown(extensions=extensions, extension_configs=extension_configs, output_format="html5") + + def convert(self, data): + """Convert data to HTML and reset internal state.""" + result = self.markdown.convert(data) + try: + meta = {} + for k in self.markdown.Meta: # This reads everything as lists + meta[k.lower()] = ','.join(self.markdown.Meta[k]) + except Exception: + meta = {} + self.markdown.reset() + return result, meta class CompileMarkdown(PageCompiler): @@ -49,42 +70,61 @@ class CompileMarkdown(PageCompiler): name = "markdown" friendly_name = "Markdown" demote_headers = True - extensions = [] site = None + supports_metadata = False def set_site(self, site): """Set Nikola site.""" - super(CompileMarkdown, self).set_site(site) + super().set_site(site) self.config_dependencies = [] + extensions = [] for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) - self.extensions.append(plugin_info.plugin_object) + 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")))) + site_extensions = self.site.config.get("MARKDOWN_EXTENSIONS") + self.config_dependencies.append(str(sorted(site_extensions))) + extensions.extend(site_extensions) + + site_extension_configs = self.site.config.get("MARKDOWN_EXTENSION_CONFIGS") + if site_extension_configs: + self.config_dependencies.append(json.dumps(site_extension_configs.values, sort_keys=True)) + + if Markdown is not None: + self.converters = {} + for lang in self.site.config['TRANSLATIONS']: + lang_extension_configs = site_extension_configs(lang) if site_extension_configs else {} + self.converters[lang] = ThreadLocalMarkdown(extensions, lang_extension_configs) + self.supports_metadata = 'markdown.extensions.meta' in extensions + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile Markdown into HTML strings.""" + if lang is None: + lang = LocaleBorg().current_lang + if Markdown is None: + req_missing(['markdown'], 'build this site (compile Markdown)') + if not is_two_file: + _, data = self.split_metadata(data, post, lang) + new_data, shortcodes = sc.extract_shortcodes(data) + output, _ = self.converters[lang].convert(new_data) + output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + return output, shortcode_deps - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" - if markdown is None: + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the 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)) - self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS") - try: - post = self.site.post_per_input_file[source] - except KeyError: - post = None - with io.open(dest, "w+", encoding="utf8") as out_file: - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - if not is_two_file: - _, data = self.split_metadata(data) - output = markdown(data, self.extensions, output_format="html5") - output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post)) + output, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(output) if post is None: if shortcode_deps: self.logger.error( - "Cannot save dependencies for post {0} due to unregistered source file name", + "Cannot save dependencies for post {0} (post unknown)", source) else: post._depfile[dest] += shortcode_deps @@ -102,9 +142,30 @@ class CompileMarkdown(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write('<!-- \n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) + + def read_metadata(self, post, lang=None): + """Read the metadata from a post, and return a metadata dict.""" + lang = lang or self.site.config['DEFAULT_LANG'] + if not self.supports_metadata: + return {} + if Markdown is None: + req_missing(['markdown'], 'build this site (compile Markdown)') + if lang is None: + lang = LocaleBorg().current_lang + source = post.translated_source_path(lang) + with io.open(source, 'r', encoding='utf-8-sig') as inf: + # Note: markdown meta returns lowercase keys + data = inf.read() + # If the metadata starts with "---" it's actually YAML and + # we should not let markdown parse it, because it will do + # bad things like setting empty tags to "''" + if data.startswith('---\n'): + return {} + _, meta = self.converters[lang].convert(data) + # Map metadata from other platforms to names Nikola expects (Issue #2817) + map_metadata(meta, 'markdown_metadata', self.site.config) + return meta diff --git a/nikola/plugins/compile/markdown/mdx_gist.plugin b/nikola/plugins/compile/markdown/mdx_gist.plugin index 85b5450..f962cb7 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.plugin +++ b/nikola/plugins/compile/markdown/mdx_gist.plugin @@ -4,7 +4,7 @@ module = mdx_gist [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index 25c071f..f6ce20a 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -75,7 +75,10 @@ Error Case: non-existent file: [:gist: 4747847 doesntexist.py] """ -from __future__ import unicode_literals, print_function +import requests + +from nikola.plugin_categories import MarkdownExtension +from nikola.utils import get_logger try: from markdown.extensions import Extension @@ -87,12 +90,8 @@ except ImportError: # the markdown compiler will fail first Extension = Pattern = object -from nikola.plugin_categories import MarkdownExtension -from nikola.utils import get_logger, STDERR_HANDLER -import requests - -LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER) +LOGGER = get_logger('compile_markdown.mdx_gist') GIST_JS_URL = "https://gist.github.com/{0}.js" GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}" @@ -167,7 +166,7 @@ class GistPattern(Pattern): pre_elem.text = AtomicString(raw_gist) except GistFetchException as e: - LOGGER.warn(e.message) + LOGGER.warning(e.message) warning_comment = etree.Comment(' WARNING: {0} '.format(e.message)) noscript_elem.append(warning_comment) @@ -186,15 +185,15 @@ class GistExtension(MarkdownExtension, Extension): for key, value in configs: self.setConfig(key, value) - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md, md_globals=None): """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") + md.inlinePatterns.register(gist_md_pattern, 'gist', 175) gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs()) gist_rst_pattern.md = md - md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist") + md.inlinePatterns.register(gist_rst_pattern, 'gist-rst', 176) md.registerExtension(self) @@ -203,6 +202,7 @@ def makeExtension(configs=None): # pragma: no cover """Make Markdown extension.""" return GistExtension(configs) + if __name__ == '__main__': import doctest diff --git a/nikola/plugins/compile/markdown/mdx_nikola.plugin b/nikola/plugins/compile/markdown/mdx_nikola.plugin index 3c5c638..9751598 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.plugin +++ b/nikola/plugins/compile/markdown/mdx_nikola.plugin @@ -4,7 +4,7 @@ module = mdx_nikola [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py index 59a5d5b..06a6d9a 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.py +++ b/nikola/plugins/compile/markdown/mdx_nikola.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -30,8 +30,10 @@ - Strikethrough inline patterns. """ -from __future__ import unicode_literals import re + +from nikola.plugin_categories import MarkdownExtension + try: from markdown.postprocessors import Postprocessor from markdown.inlinepatterns import SimpleTagPattern @@ -41,8 +43,6 @@ except ImportError: # the markdown compiler will fail first Postprocessor = SimpleTagPattern = Extension = object -from nikola.plugin_categories import MarkdownExtension - CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL) STRIKE_RE = r"(~{2})(.+?)(~{2})" # ~~strike~~ @@ -68,14 +68,14 @@ class NikolaExtension(MarkdownExtension, Extension): def _add_nikola_post_processor(self, md): """Extend Markdown with the postprocessor.""" pp = NikolaPostProcessor() - md.postprocessors.add('nikola_post_processor', pp, '_end') + md.postprocessors.register(pp, 'nikola_post_processor', 1) def _add_strikethrough_inline_pattern(self, md): """Support PHP-Markdown style strikethrough, for example: ``~~strike~~``.""" pattern = SimpleTagPattern(STRIKE_RE, 'del') - md.inlinePatterns.add('strikethrough', pattern, '_end') + md.inlinePatterns.register(pattern, 'strikethrough', 175) - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md, md_globals=None): """Extend markdown to Nikola flavours.""" self._add_nikola_post_processor(md) self._add_strikethrough_inline_pattern(md) diff --git a/nikola/plugins/compile/markdown/mdx_podcast.plugin b/nikola/plugins/compile/markdown/mdx_podcast.plugin index c4ee7e9..df5260d 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.plugin +++ b/nikola/plugins/compile/markdown/mdx_podcast.plugin @@ -4,7 +4,7 @@ module = mdx_podcast [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py index 96a70ed..5090407 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.py +++ b/nikola/plugins/compile/markdown/mdx_podcast.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013-2016 Michael Rabbitt, Roberto Alsina and others. +# Copyright © 2013-2020 Michael Rabbitt, Roberto Alsina and others. # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -36,7 +36,6 @@ Basic Example: <p><audio controls=""><source src="https://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 @@ -69,7 +68,7 @@ class PodcastPattern(Pattern): class PodcastExtension(MarkdownExtension, Extension): - """"Podcast extension for Markdown.""" + """Podcast extension for Markdown.""" def __init__(self, configs={}): """Initialize extension.""" @@ -80,11 +79,11 @@ class PodcastExtension(MarkdownExtension, Extension): for key, value in configs: self.setConfig(key, value) - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md, md_globals=None): """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.inlinePatterns.register(podcast_md_pattern, 'podcast', 175) md.registerExtension(self) @@ -92,6 +91,7 @@ def makeExtension(configs=None): # pragma: no cover """Make Markdown extension.""" return PodcastExtension(configs) + if __name__ == '__main__': import doctest doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE + diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin index 2a69095..8f339e4 100644 --- a/nikola/plugins/compile/pandoc.plugin +++ b/nikola/plugins/compile/pandoc.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Compile markups into HTML using pandoc [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = Pandoc diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 2368ae9..af14344 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,12 +24,11 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on pandoc. +"""Page compiler plugin for pandoc. You will need, of course, to install pandoc """ -from __future__ import unicode_literals import io import os @@ -48,25 +47,21 @@ class CompilePandoc(PageCompiler): def set_site(self, site): """Set Nikola site.""" self.config_dependencies = [str(site.config['PANDOC_OPTIONS'])] - super(CompilePandoc, self).set_site(site) + super().set_site(site) - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) try: - try: - post = self.site.post_per_input_file[source] - except KeyError: - post = None subprocess.check_call(['pandoc', '-o', dest, source] + self.site.config['PANDOC_OPTIONS']) - with open(dest, 'r', encoding='utf-8') as inf: - output, shortcode_deps = self.site.apply_shortcodes(inf.read(), with_dependencies=True) + with open(dest, 'r', encoding='utf-8-sig') as inf: + output, shortcode_deps = self.site.apply_shortcodes(inf.read()) with open(dest, 'w', encoding='utf-8') as outf: outf.write(output) if post is None: if shortcode_deps: self.logger.error( - "Cannot save dependencies for post {0} due to unregistered source file name", + "Cannot save dependencies for post {0} (post unknown)", source) else: post._depfile[dest] += shortcode_deps @@ -74,6 +69,10 @@ class CompilePandoc(PageCompiler): if e.strreror == 'No such file or directory': req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False) + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile into HTML strings.""" + raise ValueError("Pandoc compiler does not support compile_string due to multiple output formats") + def create_post(self, path, **kw): """Create a new post.""" content = kw.pop('content', None) @@ -88,7 +87,5 @@ class CompilePandoc(PageCompiler): content += '\n' with io.open(path, "w+", encoding="utf8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin index f4fb0c1..13384bd 100644 --- a/nikola/plugins/compile/php.plugin +++ b/nikola/plugins/compile/php.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Compile PHP into HTML (just copy and name the file .php) [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = PHP diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index d2559fd..818e10d 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,16 +24,14 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html for HTML+php.""" +"""Page compiler plugin for PHP.""" -from __future__ import unicode_literals - -import os import io +import os +from hashlib import md5 from nikola.plugin_categories import PageCompiler from nikola.utils import makedirs, write_metadata -from hashlib import md5 class CompilePhp(PageCompiler): @@ -42,8 +40,8 @@ class CompilePhp(PageCompiler): name = "php" friendly_name = "PHP" - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the 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 +49,10 @@ class CompilePhp(PageCompiler): out_file.write('<!-- __NIKOLA_PHP_TEMPLATE_INJECTION source:{0} checksum:{1}__ -->'.format(source, hash)) return True + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile PHP into HTML strings.""" + return data, [] + def create_post(self, path, **kw): """Create a new post.""" content = kw.pop('content', None) @@ -76,9 +78,7 @@ class CompilePhp(PageCompiler): content += '\n' with io.open(path, "w+", encoding="utf8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) def extension(self): diff --git a/nikola/plugins/compile/rest.plugin b/nikola/plugins/compile/rest.plugin index 4d9041a..43bdf2d 100644 --- a/nikola/plugins/compile/rest.plugin +++ b/nikola/plugins/compile/rest.plugin @@ -6,8 +6,8 @@ module = rest author = Roberto Alsina version = 1.0 website = https://getnikola.com/ -description = Compile reSt into HTML +description = Compile reST into HTML [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = reStructuredText diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py index b75849f..44da076 100644 --- a/nikola/plugins/compile/rest/__init__.py +++ b/nikola/plugins/compile/rest/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,28 +26,28 @@ """reStructuredText compiler for Nikola.""" -from __future__ import unicode_literals import io +import logging import os import docutils.core import docutils.nodes +import docutils.transforms import docutils.utils import docutils.io import docutils.readers.standalone -import docutils.writers.html4css1 +import docutils.writers.html5_polyglot import docutils.parsers.rst.directives from docutils.parsers.rst import roles from nikola.nikola import LEGAL_VALUES +from nikola.metadata_extractors import MetaCondition from nikola.plugin_categories import PageCompiler from nikola.utils import ( - unicode_str, - get_logger, makedirs, write_metadata, - STDERR_HANDLER, - LocaleBorg + LocaleBorg, + map_metadata ) @@ -58,15 +58,57 @@ class CompileRest(PageCompiler): friendly_name = "reStructuredText" demote_headers = True logger = None - - def compile_html_string(self, data, source_path=None, is_two_file=True): + supports_metadata = True + metadata_conditions = [(MetaCondition.config_bool, "USE_REST_DOCINFO_METADATA")] + + def read_metadata(self, post, lang=None): + """Read the metadata from a post, and return a metadata dict.""" + if lang is None: + lang = LocaleBorg().current_lang + source_path = post.translated_source_path(lang) + + # Silence reST errors, some of which are due to a different + # environment. Real issues will be reported while compiling. + null_logger = logging.getLogger('NULL') + null_logger.setLevel(1000) + with io.open(source_path, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + _, _, _, document = rst2html(data, logger=null_logger, source_path=source_path, transforms=self.site.rst_transforms) + meta = {} + if 'title' in document: + meta['title'] = document['title'] + for docinfo in document.traverse(docutils.nodes.docinfo): + for element in docinfo.children: + if element.tagname == 'field': # custom fields (e.g. summary) + name_elem, body_elem = element.children + name = name_elem.astext() + value = body_elem.astext() + elif element.tagname == 'authors': # author list + name = element.tagname + value = [element.astext() for element in element.children] + else: # standard fields (e.g. address) + name = element.tagname + value = element.astext() + name = name.lower() + + meta[name] = value + + # Put 'authors' meta field contents in 'author', too + if 'authors' in meta and 'author' not in meta: + meta['author'] = '; '.join(meta['authors']) + + # Map metadata from other platforms to names Nikola expects (Issue #2817) + map_metadata(meta, 'rest_docinfo', self.site.config) + return meta + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): """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). add_ln = 0 if not is_two_file: - m_data, data = self.split_metadata(data) + m_data, data = self.split_metadata(data, post, lang) add_ln = len(m_data.splitlines()) + 1 default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt') @@ -76,38 +118,42 @@ class CompileRest(PageCompiler): 'stylesheet_path': None, 'link_stylesheet': True, 'syntax_highlight': 'short', - 'math_output': 'mathjax', + # This path is not used by Nikola, but we need something to silence + # warnings about it from reST. + 'math_output': 'mathjax /assets/js/mathjax.js', 'template': default_template_path, - 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en') + 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en'), + 'doctitle_xform': self.site.config.get('USE_REST_DOCINFO_METADATA'), + 'file_insertion_enabled': self.site.config.get('REST_FILE_INSERTION_ENABLED'), } - output, error_level, deps = rst2html( - data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms, - no_title_transform=self.site.config.get('NO_DOCUTILS_TITLE_TRANSFORM', False)) - if not isinstance(output, unicode_str): + from nikola import shortcodes as sc + new_data, shortcodes = sc.extract_shortcodes(data) + if self.site.config.get('HIDE_REST_DOCINFO', False): + self.site.rst_transforms.append(RemoveDocinfo) + output, error_level, deps, _ = rst2html( + new_data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms) + if not isinstance(output, str): # To prevent some weird bugs here or there. # Original issue: empty files. `output` became a bytestring. output = output.decode('utf-8') - return output, error_level, deps - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + return output, error_level, deps, shortcode_deps + + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the 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: - try: - post = self.site.post_per_input_file[source] - except KeyError: - post = None - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - output, error_level, deps = self.compile_html_string(data, source, is_two_file) - output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post)) + output, error_level, deps, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(output) if post is None: if deps.list: self.logger.error( - "Cannot save dependencies for post {0} due to unregistered source file name", + "Cannot save dependencies for post {0} (post unknown)", source) else: post._depfile[dest] += deps.list @@ -129,23 +175,21 @@ class CompileRest(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write(write_metadata(metadata)) - fd.write('\n') + fd.write(write_metadata(metadata, comment_wrap=False, site=self.site, compiler=self)) fd.write(content) def set_site(self, site): """Set Nikola site.""" - super(CompileRest, self).set_site(site) + super().set_site(site) self.config_dependencies = [] for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) plugin_info.plugin_object.short_help = plugin_info.description - self.logger = get_logger('compile_rest', STDERR_HANDLER) if not site.debug: - self.logger.level = 4 + self.logger.level = logging.WARNING def get_observer(settings): @@ -155,19 +199,25 @@ def get_observer(settings): Error code mapping: - +------+---------+------+----------+ - | dNUM | dNAME | lNUM | lNAME | d = docutils, l = logbook - +------+---------+------+----------+ - | 0 | DEBUG | 1 | DEBUG | - | 1 | INFO | 2 | INFO | - | 2 | WARNING | 4 | WARNING | - | 3 | ERROR | 5 | ERROR | - | 4 | SEVERE | 6 | CRITICAL | - +------+---------+------+----------+ + +----------+----------+ + | docutils | logging | + +----------+----------+ + | DEBUG | DEBUG | + | INFO | INFO | + | WARNING | WARNING | + | ERROR | ERROR | + | SEVERE | CRITICAL | + +----------+----------+ """ - errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6} + errormap = { + docutils.utils.Reporter.DEBUG_LEVEL: logging.DEBUG, + docutils.utils.Reporter.INFO_LEVEL: logging.INFO, + docutils.utils.Reporter.WARNING_LEVEL: logging.WARNING, + docutils.utils.Reporter.ERROR_LEVEL: logging.ERROR, + docutils.utils.Reporter.SEVERE_LEVEL: logging.CRITICAL + } text = docutils.nodes.Element.astext(msg) - line = msg['line'] + settings['add_ln'] if 'line' in msg else 0 + line = msg['line'] + settings['add_ln'] if 'line' in msg else '' out = '[{source}{colon}{line}] {text}'.format( source=settings['source'], colon=(':' if line else ''), line=line, text=text) @@ -179,32 +229,32 @@ def get_observer(settings): class NikolaReader(docutils.readers.standalone.Reader): """Nikola-specific docutils reader.""" + config_section = 'nikola' + def __init__(self, *args, **kwargs): """Initialize the reader.""" self.transforms = kwargs.pop('transforms', []) - self.no_title_transform = kwargs.pop('no_title_transform', False) + self.logging_settings = kwargs.pop('nikola_logging_settings', {}) docutils.readers.standalone.Reader.__init__(self, *args, **kwargs) def get_transforms(self): """Get docutils transforms.""" - transforms = docutils.readers.standalone.Reader(self).get_transforms() + self.transforms - if self.no_title_transform: - transforms = [t for t in transforms if str(t) != "<class 'docutils.transforms.frontmatter.DocTitle'>"] - return transforms + return docutils.readers.standalone.Reader(self).get_transforms() + self.transforms def new_document(self): """Create and return a new empty document tree (root node).""" document = docutils.utils.new_document(self.source.source_path, self.settings) document.reporter.stream = False - document.reporter.attach_observer(get_observer(self.l_settings)) + document.reporter.attach_observer(get_observer(self.logging_settings)) return document def shortcode_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """A shortcode role that passes through raw inline HTML.""" + """Return a shortcode role that passes through raw inline HTML.""" return [docutils.nodes.raw('', text, format='html')], [] + roles.register_canonical_role('raw-html', shortcode_role) roles.register_canonical_role('html', shortcode_role) roles.register_canonical_role('sc', shortcode_role) @@ -226,7 +276,7 @@ def add_node(node, visit_function=None, depart_function=None): self.site = site directives.register_directive('math', MathDirective) add_node(MathBlock, visit_Math, depart_Math) - return super(Plugin, self).set_site(site) + return super().set_site(site) class MathDirective(Directive): def run(self): @@ -245,18 +295,53 @@ def add_node(node, visit_function=None, depart_function=None): """ docutils.nodes._add_node_class_names([node.__name__]) if visit_function: - setattr(docutils.writers.html4css1.HTMLTranslator, 'visit_' + node.__name__, visit_function) + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_' + node.__name__, visit_function) if depart_function: - setattr(docutils.writers.html4css1.HTMLTranslator, 'depart_' + node.__name__, depart_function) + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'depart_' + node.__name__, depart_function) + + +# Output <code> for ``double backticks``. (Code and extra logic based on html4css1 translator) +def visit_literal(self, node): + """Output <code> for double backticks.""" + # special case: "code" role + classes = node.get('classes', []) + if 'code' in classes: + # filter 'code' from class arguments + node['classes'] = [cls for cls in classes if cls != 'code'] + self.body.append(self.starttag(node, 'code', '')) + return + self.body.append( + self.starttag(node, 'code', '', CLASS='docutils literal')) + text = node.astext() + for token in self.words_and_spaces.findall(text): + if token.strip(): + # Protect text like "--an-option" and the regular expression + # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping + if self.in_word_wrap_point.search(token): + self.body.append('<span class="pre">%s</span>' + % self.encode(token)) + else: + self.body.append(self.encode(token)) + elif token in ('\n', ' '): + # Allow breaks at whitespace: + self.body.append(token) + else: + # Protect runs of multiple spaces; the last space can wrap: + self.body.append(' ' * (len(token) - 1) + ' ') + self.body.append('</code>') + # Content already processed: + raise docutils.nodes.SkipNode + + +setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_literal', visit_literal) def rst2html(source, source_path=None, source_class=docutils.io.StringInput, destination_path=None, reader=None, parser=None, parser_name='restructuredtext', writer=None, - writer_name='html', settings=None, settings_spec=None, - settings_overrides=None, config_section=None, - enable_exit_status=None, logger=None, l_add_ln=0, transforms=None, - no_title_transform=False): + writer_name='html5_polyglot', settings=None, settings_spec=None, + settings_overrides=None, config_section='nikola', + enable_exit_status=None, logger=None, l_add_ln=0, transforms=None): """Set up & run a ``Publisher``, and return a dictionary of document parts. Dictionary keys are the names of parts, and values are Unicode strings; @@ -268,20 +353,22 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput, publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) - Parameters: see `publish_programmatically`. + For a description of the parameters, see `publish_programmatically`. WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report reStructuredText syntax errors. """ if reader is None: - reader = NikolaReader(transforms=transforms, no_title_transform=no_title_transform) # For our custom logging, we have special needs and special settings we # specify here. # logger a logger from Nikola # source source filename (docutils gets a string) - # add_ln amount of metadata lines (see comment in compile_html above) - reader.l_settings = {'logger': logger, 'source': source_path, - 'add_ln': l_add_ln} + # add_ln amount of metadata lines (see comment in CompileRest.compile above) + reader = NikolaReader(transforms=transforms, + nikola_logging_settings={ + 'logger': logger, 'source': source_path, + 'add_ln': l_add_ln + }) pub = docutils.core.Publisher(reader, parser, writer, settings=settings, source_class=source_class, @@ -294,7 +381,8 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput, pub.set_destination(None, destination_path) pub.publish(enable_exit_status=enable_exit_status) - return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies + return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies, pub.document + # Alignment helpers for extensions _align_options_base = ('left', 'center', 'right') @@ -302,3 +390,14 @@ _align_options_base = ('left', 'center', 'right') def _align_choice(argument): return docutils.parsers.rst.directives.choice(argument, _align_options_base + ("none", "")) + + +class RemoveDocinfo(docutils.transforms.Transform): + """Remove docinfo nodes.""" + + default_priority = 870 + + def apply(self): + """Remove docinfo nodes.""" + for node in self.document.traverse(docutils.nodes.docinfo): + node.parent.remove(node) diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin index 0a7896f..4434477 100644 --- a/nikola/plugins/compile/rest/chart.plugin +++ b/nikola/plugins/compile/rest/chart.plugin @@ -4,7 +4,7 @@ module = chart [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py index 24f459b..17363cb 100644 --- a/nikola/plugins/compile/rest/chart.py +++ b/nikola/plugins/compile/rest/chart.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -23,21 +23,17 @@ # 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. - """Chart directive for reSTructuredText.""" -from ast import literal_eval - from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension + try: import pygal except ImportError: - pygal = None # NOQA - -from nikola.plugin_categories import RestExtension -from nikola.utils import req_missing + pygal = None _site = None @@ -52,8 +48,7 @@ class Plugin(RestExtension): global _site _site = self.site = site directives.register_directive('chart', Chart) - self.site.register_shortcode('chart', _gen_chart) - return super(Plugin, self).set_site(site) + return super().set_site(site) class Chart(Directive): @@ -77,6 +72,7 @@ class Chart(Directive): "classes": directives.unchanged, "css": directives.unchanged, "defs": directives.unchanged, + "data_file": directives.unchanged, "disable_xml_declaration": directives.unchanged, "dots_size": directives.unchanged, "dynamic_print_values": directives.unchanged, @@ -157,41 +153,9 @@ class Chart(Directive): def run(self): """Run the directive.""" self.options['site'] = None - html = _gen_chart(self.arguments[0], data='\n'.join(self.content), **self.options) + html = _site.plugin_manager.getPluginByName( + 'chart', 'ShortcodePlugin').plugin_object.handler( + self.arguments[0], + data='\n'.join(self.content), + **self.options) return [nodes.raw('', html, format='html')] - - -def _gen_chart(chart_type, **_options): - if pygal is None: - msg = req_missing(['pygal'], 'use the Chart directive', optional=True) - return '<div class="text-error">{0}</div>'.format(msg) - options = {} - data = _options.pop('data') - _options.pop('post', None) - _options.pop('site') - if 'style' in _options: - style_name = _options.pop('style') - else: - style_name = 'BlueStyle' - if '(' in style_name: # Parametric style - style = eval('pygal.style.' + style_name) - else: - style = getattr(pygal.style, style_name) - for k, v in _options.items(): - try: - options[k] = literal_eval(v) - except: - options[k] = v - chart = pygal - for o in chart_type.split('.'): - chart = getattr(chart, o) - chart = chart(style=style) - if _site and _site.invariant: - chart.no_prefix = True - chart.config(**options) - for line in data.splitlines(): - line = line.strip() - if line: - label, series = literal_eval('({0})'.format(line)) - chart.add(label, series) - return chart.render().decode('utf8') diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin index e447eb2..3b5c9c7 100644 --- a/nikola/plugins/compile/rest/doc.plugin +++ b/nikola/plugins/compile/rest/doc.plugin @@ -4,7 +4,7 @@ module = doc [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Manuel Kaufmann diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py index 55f576d..705c0bc 100644 --- a/nikola/plugins/compile/rest/doc.py +++ b/nikola/plugins/compile/rest/doc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,7 +29,7 @@ from docutils import nodes from docutils.parsers.rst import roles -from nikola.utils import split_explicit_title, LOGGER +from nikola.utils import split_explicit_title, LOGGER, slugify from nikola.plugin_categories import RestExtension @@ -44,14 +44,11 @@ class Plugin(RestExtension): roles.register_canonical_role('doc', doc_role) self.site.register_shortcode('doc', doc_shortcode) doc_role.site = site - return super(Plugin, self).set_site(site) + return super().set_site(site) -def _doc_link(rawtext, text, 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 +def _find_post(slug): + """Find a post with the given slug in posts or pages.""" twin_slugs = False post = None for p in doc_role.site.timeline: @@ -61,10 +58,27 @@ def _doc_link(rawtext, text, options={}, content=[]): else: twin_slugs = True break + return post, twin_slugs + + +def _doc_link(rawtext, text, 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) + if '#' in slug: + slug, fragment = slug.split('#', 1) + else: + fragment = None + + # Look for the unslugified input first, then try to slugify (Issue #3450) + post, twin_slugs = _find_post(slug) + if post is None: + slug = slugify(slug) + post, twin_slugs = _find_post(slug) try: if post is None: - raise ValueError + raise ValueError("No post with matching slug found.") except ValueError: return False, False, None, None, slug @@ -72,6 +86,8 @@ def _doc_link(rawtext, text, options={}, content=[]): # use post's title as link's text title = post.title() permalink = post.permalink() + if fragment: + permalink += '#' + fragment return True, twin_slugs, title, permalink, slug @@ -83,7 +99,7 @@ def doc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): if twin_slugs: inliner.reporter.warning( 'More than one post with the same slug. Using "{0}"'.format(permalink)) - LOGGER.warn( + LOGGER.warning( 'More than one post with the same slug. Using "{0}" for doc role'.format(permalink)) node = make_link_node(rawtext, title, permalink, options) return [node], [] @@ -101,7 +117,7 @@ def doc_shortcode(*args, **kwargs): success, twin_slugs, title, permalink, slug = _doc_link(text, text, LOGGER) if success: if twin_slugs: - LOGGER.warn( + LOGGER.warning( 'More than one post with the same slug. Using "{0}" for doc shortcode'.format(permalink)) return '<a href="{0}">{1}</a>'.format(permalink, title) else: diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin index 763c1d2..4a8a3a7 100644 --- a/nikola/plugins/compile/rest/gist.plugin +++ b/nikola/plugins/compile/rest/gist.plugin @@ -4,7 +4,7 @@ module = gist [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/rest/gist.py b/nikola/plugins/compile/rest/gist.py index e40c3b2..08aa46d 100644 --- a/nikola/plugins/compile/rest/gist.py +++ b/nikola/plugins/compile/rest/gist.py @@ -19,7 +19,7 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('gist', GitHubGist) - return super(Plugin, self).set_site(site) + return super().set_site(site) class GitHubGist(Directive): diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin index 3ebb296..5239f92 100644 --- a/nikola/plugins/compile/rest/listing.plugin +++ b/nikola/plugins/compile/rest/listing.plugin @@ -4,7 +4,7 @@ module = listing [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index 4dfbedc..e5a73fa 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -28,26 +28,21 @@ """Define and register a listing directive using the existing CodeBlock.""" -from __future__ import unicode_literals import io import os import uuid -try: - from urlparse import urlunsplit -except ImportError: - from urllib.parse import urlunsplit # NOQA +from urllib.parse import urlunsplit import docutils.parsers.rst.directives.body import docutils.parsers.rst.directives.misc +import pygments +import pygments.util from docutils import core from docutils import nodes from docutils.parsers.rst import Directive, directives from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst.directives.misc import Include - from pygments.lexers import get_lexer_by_name -import pygments -import pygments.util from nikola import utils from nikola.plugin_categories import RestExtension @@ -119,6 +114,7 @@ class CodeBlock(Directive): return [node] + # Monkey-patch: replace insane docutils CodeBlock with our implementation. docutils.parsers.rst.directives.body.CodeBlock = CodeBlock docutils.parsers.rst.directives.misc.CodeBlock = CodeBlock @@ -142,7 +138,7 @@ class Plugin(RestExtension): directives.register_directive('sourcecode', CodeBlock) directives.register_directive('listing', Listing) Listing.folders = site.config['LISTINGS_FOLDERS'] - return super(Plugin, self).set_site(site) + return super().set_site(site) # Add sphinx compatibility option @@ -186,7 +182,7 @@ class Listing(Include): self.arguments.insert(0, fpath) if 'linenos' in self.options: self.options['number-lines'] = self.options['linenos'] - with io.open(fpath, 'r+', encoding='utf8') as fileobject: + with io.open(fpath, 'r+', encoding='utf-8-sig') as fileobject: self.content = fileobject.read().splitlines() self.state.document.settings.record_dependencies.add(fpath) target = urlunsplit(("link", 'listing', fpath.replace('\\', '/'), '', '')) @@ -200,8 +196,11 @@ class Listing(Include): def get_code_from_file(self, data): """Create CodeBlock nodes from file object content.""" - return super(Listing, self).run() + return super().run() def assert_has_content(self): - """Listing has no content, override check from superclass.""" + """Override check from superclass with nothing. + + 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 8dfb19c..396c2f9 100644 --- a/nikola/plugins/compile/rest/media.plugin +++ b/nikola/plugins/compile/rest/media.plugin @@ -4,7 +4,7 @@ module = media [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py index 8a69586..d29d0a2 100644 --- a/nikola/plugins/compile/rest/media.py +++ b/nikola/plugins/compile/rest/media.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,14 +29,13 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + try: import micawber except ImportError: - micawber = None # NOQA - - -from nikola.plugin_categories import RestExtension -from nikola.utils import req_missing + micawber = None class Plugin(RestExtension): @@ -49,7 +48,7 @@ class Plugin(RestExtension): self.site = site directives.register_directive('media', Media) self.site.register_shortcode('media', _gen_media_embed) - return super(Plugin, self).set_site(site) + return super().set_site(site) class Media(Directive): diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin index 1802f2b..68abaef 100644 --- a/nikola/plugins/compile/rest/post_list.plugin +++ b/nikola/plugins/compile/rest/post_list.plugin @@ -4,11 +4,11 @@ module = post_list [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Udo Spallek -version = 0.1 +version = 0.2 website = https://getnikola.com/ -description = Includes a list of posts with tag and slide based filters. +description = Includes a list of posts with tag and slice based filters. diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py index 8cfd5bf..f7e95ed 100644 --- a/nikola/plugins/compile/rest/post_list.py +++ b/nikola/plugins/compile/rest/post_list.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2013-2016 Udo Spallek, Roberto Alsina and others. +# Copyright © 2013-2020 Udo Spallek, Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -23,21 +23,13 @@ # 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 -import uuid -import natsort - from docutils import nodes from docutils.parsers.rst import Directive, directives from nikola import utils from nikola.plugin_categories import RestExtension -from nikola.packages.datecond import date_in_range # WARNING: the directive name is post-list # (with a DASH instead of an UNDERSCORE) @@ -51,91 +43,14 @@ class Plugin(RestExtension): def set_site(self, site): """Set Nikola site.""" self.site = site - self.site.register_shortcode('post-list', _do_post_list) - directives.register_directive('post-list', PostList) - PostList.site = site - return super(Plugin, self).set_site(site) - - -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, date, tags, categories, sections, slugs, post_type, all, template, id - :Directive Content: None. - - The posts appearing in the list can be filtered by options. - *List slicing* is provided with the *start*, *stop* and *reverse* options. - - The following not required options are recognized: - - ``start`` : integer - The index of the first post to show. - A negative value like ``-3`` will show the *last* three posts in the - post-list. - Defaults to None. - - ``stop`` : integer - The index of the last post to show. - A value negative value like ``-1`` will show every post, but not the - *last* in the post-list. - Defaults to None. - - ``reverse`` : flag - Reverse the order of the post-list. - Defaults is to not reverse the order of posts. - - ``sort`` : string - Sort post list by one of each post's attributes, usually ``title`` or a - custom ``priority``. Defaults to None (chronological sorting). + directives.register_directive('post-list', PostListDirective) + directives.register_directive('post_list', PostListDirective) + PostListDirective.site = site + return super().set_site(site) - ``date`` : string - Show posts that match date range specified by this option. Format: - * comma-separated clauses (AND) - * clause: attribute comparison_operator value (spaces optional) - * attribute: year, month, day, hour, month, second, weekday, isoweekday; or empty for full datetime - * comparison_operator: == != <= >= < > - * value: integer or dateutil-compatible date input - - ``tags`` : string [, string...] - 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. - - ``sections`` : string [, string...] - Filter posts to show only posts having one of the ``sections``. - Defaults to None. - - ``slugs`` : string [, string...] - Filter posts to show only posts having at least one of the ``slugs``. - Defaults to None. - - ``post_type`` (or ``type``) : string - Show only ``posts``, ``pages`` or ``all``. - Replaces ``all``. Defaults to ``posts``. - - ``all`` : flag - (deprecated, use ``post_type`` instead) - Shows all posts and pages in the post list. Defaults to show only posts. - - ``lang`` : string - The language of post *titles* and *links*. - Defaults to default language. - - ``template`` : string - The name of an alternative template to render the post-list. - Defaults to ``post_list_directive.tmpl`` - - ``id`` : string - A manual id for the post list. - Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex. - """ +class PostListDirective(Directive): + """Provide a reStructuredText directive to create a list of posts.""" option_spec = { 'start': int, @@ -143,12 +58,12 @@ class PostList(Directive): 'reverse': directives.flag, 'sort': directives.unchanged, 'tags': directives.unchanged, + 'require_all_tags': directives.flag, 'categories': directives.unchanged, 'sections': directives.unchanged, 'slugs': directives.unchanged, 'post_type': directives.unchanged, 'type': directives.unchanged, - 'all': directives.flag, 'lang': directives.unchanged, 'template': directives.path, 'id': directives.unchanged, @@ -161,151 +76,42 @@ class PostList(Directive): stop = self.options.get('stop') reverse = self.options.get('reverse', False) tags = self.options.get('tags') + require_all_tags = 'require_all_tags' in self.options categories = self.options.get('categories') sections = self.options.get('sections') slugs = self.options.get('slugs') post_type = self.options.get('post_type') type = self.options.get('type', False) - all = self.options.get('all', False) lang = self.options.get('lang', utils.LocaleBorg().current_lang) template = self.options.get('template', 'post_list_directive.tmpl') sort = self.options.get('sort') date = self.options.get('date') - - output, deps = _do_post_list(start, stop, reverse, tags, categories, sections, slugs, post_type, type, - all, lang, template, sort, state=self.state, site=self.site, date=date) - self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE") + filename = self.state.document.settings._nikola_source_path + + output, deps = self.site.plugin_manager.getPluginByName( + 'post_list', 'ShortcodePlugin').plugin_object.handler( + start, + stop, + reverse, + tags, + require_all_tags, + categories, + sections, + slugs, + post_type, + type, + lang, + template, + sort, + state=self.state, + site=self.site, + date=date, + filename=filename) + self.state.document.settings.record_dependencies.add( + "####MAGIC####TIMELINE") for d in deps: self.state.document.settings.record_dependencies.add(d) if output: return [nodes.raw('', output, format='html')] else: return [] - - -def _do_post_list(start=None, stop=None, reverse=False, tags=None, categories=None, - sections=None, slugs=None, post_type='post', type=False, all=False, - lang=None, template='post_list_directive.tmpl', sort=None, - id=None, data=None, state=None, site=None, date=None, filename=None, post=None): - if lang is None: - lang = utils.LocaleBorg().current_lang - if site.invariant: # for testing purposes - post_list_id = id or 'post_list_' + 'fixedvaluethatisnotauuid' - else: - post_list_id = id or 'post_list_' + uuid.uuid4().hex - - # Get post from filename if available - if filename: - self_post = site.post_per_input_file.get(filename) - else: - self_post = None - - if self_post: - self_post.register_depfile("####MAGIC####TIMELINE", lang=lang) - - # If we get strings for start/stop, make them integers - if start is not None: - start = int(start) - if stop is not None: - stop = int(stop) - - # Parse tags/categories/sections/slugs (input is strings) - tags = [t.strip().lower() for t in tags.split(',')] if tags else [] - categories = [c.strip().lower() for c in categories.split(',')] if categories else [] - sections = [s.strip().lower() for s in sections.split(',')] if sections else [] - slugs = [s.strip() for s in slugs.split(',')] if slugs else [] - - filtered_timeline = [] - posts = [] - step = -1 if reverse is None else None - - if type is not False: - post_type = type - - # TODO: remove in v8 - if all is not False: - timeline = [p for p in site.timeline] - elif post_type == 'page' or post_type == 'pages': - timeline = [p for p in site.timeline if not p.use_in_feeds] - elif post_type == 'all': - timeline = [p for p in site.timeline] - else: # post - timeline = [p for p in site.timeline if p.use_in_feeds] - - # TODO: replaces all, uncomment in v8 - # if post_type == 'page' or post_type == 'pages': - # timeline = [p for p in site.timeline if not p.use_in_feeds] - # elif post_type == 'all': - # timeline = [p for p in site.timeline] - # else: # post - # timeline = [p for p in site.timeline if p.use_in_feeds] - - if categories: - timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories] - - if sections: - timeline = [p for p in timeline if p.section_name(lang).lower() in sections] - - for post in timeline: - if tags: - cont = True - tags_lower = [t.lower() for t in post.tags] - for tag in tags: - if tag in tags_lower: - cont = False - - if cont: - continue - - filtered_timeline.append(post) - - if sort: - filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC) - - if date: - filtered_timeline = [p for p in filtered_timeline if date_in_range(date, p.date)] - - for post in filtered_timeline[start:stop:step]: - if slugs: - cont = True - for slug in slugs: - if slug == post.meta('slug'): - cont = False - - if cont: - continue - - bp = post.translated_base_path(lang) - if os.path.exists(bp) and state: - state.document.settings.record_dependencies.add(bp) - elif os.path.exists(bp) and self_post: - self_post.register_depfile(bp, lang=lang) - - posts += [post] - - if not posts: - return '', [] - - template_deps = site.template_system.template_deps(template) - if state: - # Register template as a dependency (Issue #2391) - for d in template_deps: - state.document.settings.record_dependencies.add(d) - elif self_post: - for d in template_deps: - self_post.register_depfile(d, lang=lang) - - template_data = { - 'lang': lang, - 'posts': posts, - # Need to provide str, not TranslatableSetting (Issue #2104) - 'date_format': site.GLOBAL_CONTEXT.get('date_format')[lang], - 'post_list_id': post_list_id, - 'messages': site.MESSAGES, - } - output = site.template_system.render_template( - template, None, template_data) - return output, template_deps - -# Request file name from shortcode (Issue #2412) -_do_post_list.nikola_shortcode_pass_filename = True diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin deleted file mode 100644 index 389da39..0000000 --- a/nikola/plugins/compile/rest/slides.plugin +++ /dev/null @@ -1,14 +0,0 @@ -[Core] -name = rest_slides -module = slides - -[Nikola] -compiler = rest -plugincategory = CompilerExtension - -[Documentation] -author = Roberto Alsina -version = 0.1 -website = https://getnikola.com/ -description = Slides directive - diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py deleted file mode 100644 index 7c5b34b..0000000 --- a/nikola/plugins/compile/rest/slides.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2016 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Slides directive for reStructuredText.""" - -from __future__ import unicode_literals - -import uuid - -from docutils import nodes -from docutils.parsers.rst import Directive, directives - -from nikola.plugin_categories import RestExtension - - -class Plugin(RestExtension): - """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 - return super(Plugin, self).set_site(site) - - -class Slides(Directive): - """reST extension for inserting slideshows.""" - - has_content = True - - def run(self): - """Run the slides directive.""" - if len(self.content) == 0: # pragma: no cover - return - - if self.site.invariant: # for testing purposes - carousel_id = 'slides_' + 'fixedvaluethatisnotauuid' - else: - carousel_id = 'slides_' + uuid.uuid4().hex - - output = self.site.template_system.render_template( - 'slides.tmpl', - None, - { - 'slides_content': self.content, - 'carousel_id': carousel_id, - } - ) - return [nodes.raw('', output, format='html')] - - -directives.register_directive('slides', Slides) diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin index 4e36ea4..f85a964 100644 --- a/nikola/plugins/compile/rest/soundcloud.plugin +++ b/nikola/plugins/compile/rest/soundcloud.plugin @@ -4,7 +4,7 @@ module = soundcloud [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/compile/rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py index 9fabe70..5dbcfc3 100644 --- a/nikola/plugins/compile/rest/soundcloud.py +++ b/nikola/plugins/compile/rest/soundcloud.py @@ -1,5 +1,29 @@ # -*- coding: utf-8 -*- +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """SoundCloud directive for reStructuredText.""" from docutils import nodes @@ -19,7 +43,7 @@ class Plugin(RestExtension): self.site = site directives.register_directive('soundcloud', SoundCloud) directives.register_directive('soundcloud_playlist', SoundCloudPlaylist) - return super(Plugin, self).set_site(site) + return super().set_site(site) CODE = """\ diff --git a/nikola/plugins/compile/rest/thumbnail.plugin b/nikola/plugins/compile/rest/thumbnail.plugin index 3324c31..e7b649d 100644 --- a/nikola/plugins/compile/rest/thumbnail.plugin +++ b/nikola/plugins/compile/rest/thumbnail.plugin @@ -4,7 +4,7 @@ module = thumbnail [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Pelle Nilsson diff --git a/nikola/plugins/compile/rest/thumbnail.py b/nikola/plugins/compile/rest/thumbnail.py index 37e0973..06ca9e4 100644 --- a/nikola/plugins/compile/rest/thumbnail.py +++ b/nikola/plugins/compile/rest/thumbnail.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2014-2016 Pelle Nilsson and others. +# Copyright © 2014-2020 Pelle Nilsson and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -43,7 +43,7 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('thumbnail', Thumbnail) - return super(Plugin, self).set_site(site) + return super().set_site(site) class Thumbnail(Figure): @@ -69,7 +69,7 @@ class Thumbnail(Figure): """Run the thumbnail directive.""" uri = directives.uri(self.arguments[0]) if uri.endswith('.svg'): - # the ? at the end makes docutil output an <img> instead of an object for the svg, which colorbox requires + # the ? at the end makes docutil output an <img> instead of an object for the svg, which lightboxes may require self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) + '?' else: 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 688f981..89b171b 100644 --- a/nikola/plugins/compile/rest/vimeo.plugin +++ b/nikola/plugins/compile/rest/vimeo.plugin @@ -4,7 +4,7 @@ module = vimeo [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] description = Vimeo directive diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index f1ac6c3..7047b03 100644 --- a/nikola/plugins/compile/rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,14 @@ """Vimeo directive for reStructuredText.""" -from docutils import nodes -from docutils.parsers.rst import Directive, directives -from nikola.plugins.compile.rest import _align_choice, _align_options_base - -import requests import json +import requests +from docutils import nodes +from docutils.parsers.rst import Directive, directives from nikola.plugin_categories import RestExtension +from nikola.plugins.compile.rest import _align_choice, _align_options_base class Plugin(RestExtension): @@ -46,7 +45,7 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('vimeo', Vimeo) - return super(Plugin, self).set_site(site) + return super().set_site(site) CODE = """<div class="vimeo-video{align}"> diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin index 5fbd67b..d83d0f8 100644 --- a/nikola/plugins/compile/rest/youtube.plugin +++ b/nikola/plugins/compile/rest/youtube.plugin @@ -4,7 +4,7 @@ module = youtube [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] version = 0.1 diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py index b3dde62..d52ec64 100644 --- a/nikola/plugins/compile/rest/youtube.py +++ b/nikola/plugins/compile/rest/youtube.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -28,8 +28,8 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives -from nikola.plugins.compile.rest import _align_choice, _align_options_base +from nikola.plugins.compile.rest import _align_choice, _align_options_base from nikola.plugin_categories import RestExtension @@ -42,13 +42,14 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('youtube', Youtube) - return super(Plugin, self).set_site(site) + return super().set_site(site) CODE = """\ <div class="youtube-video{align}"> <iframe width="{width}" height="{height}" -src="https://www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" +src="https://www.youtube-nocookie.com/embed/{yid}?rel=0&wmode=transparent" +frameborder="0" allow="encrypted-media" allowfullscreen ></iframe> </div>""" @@ -66,8 +67,8 @@ class Youtube(Directive): has_content = True required_arguments = 1 option_spec = { - "width": directives.positive_int, - "height": directives.positive_int, + "width": directives.unchanged, + "height": directives.unchanged, "align": _align_choice } @@ -76,10 +77,10 @@ class Youtube(Directive): self.check_content() options = { 'yid': self.arguments[0], - 'width': 425, - 'height': 344, + 'width': 560, + 'height': 315, } - options.update(self.options) + options.update({k: v for k, v in self.options.items() if v}) if self.options.get('align') in _align_options_base: options['align'] = ' align-' + self.options['align'] else: diff --git a/nikola/plugins/misc/__init__.py b/nikola/plugins/misc/__init__.py index 518fac1..1e7e6e1 100644 --- a/nikola/plugins/misc/__init__.py +++ b/nikola/plugins/misc/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/misc/scan_posts.py b/nikola/plugins/misc/scan_posts.py index f584a05..8812779 100644 --- a/nikola/plugins/misc/scan_posts.py +++ b/nikola/plugins/misc/scan_posts.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """The default post scanner.""" -from __future__ import unicode_literals, print_function import glob import os import sys @@ -35,7 +34,7 @@ from nikola.plugin_categories import PostScanner from nikola import utils from nikola.post import Post -LOGGER = utils.get_logger('scan_posts', utils.STDERR_HANDLER) +LOGGER = utils.get_logger('scan_posts') class ScanPosts(PostScanner): @@ -55,10 +54,10 @@ class ScanPosts(PostScanner): self.site.config['post_pages']: if not self.site.quiet: print(".", end='', file=sys.stderr) + destination_translatable = utils.TranslatableSetting('destination', destination, self.site.config['TRANSLATIONS']) dirname = os.path.dirname(wildcard) for dirpath, _, _ in os.walk(dirname, followlinks=True): - dest_dir = os.path.normpath(os.path.join(destination, - os.path.relpath(dirpath, dirname))) # output/destination/foo/ + rel_dest_dir = os.path.relpath(dirpath, dirname) # Get all the untranslated paths dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst untranslated = glob.glob(dir_glob) @@ -84,24 +83,30 @@ class ScanPosts(PostScanner): if not any([x.startswith('.') for x in p.split(os.sep)])] - for base_path in full_list: + for base_path in sorted(full_list): if base_path in seen: continue - else: - seen.add(base_path) try: post = Post( base_path, self.site.config, - dest_dir, + rel_dest_dir, use_in_feeds, self.site.MESSAGES, template_name, - self.site.get_compiler(base_path) + self.site.get_compiler(base_path), + destination_base=destination_translatable, + metadata_extractors_by=self.site.metadata_extractors_by ) + for lang in post.translated_to: + seen.add(post.translated_source_path(lang)) timeline.append(post) - except Exception as err: + except Exception: LOGGER.error('Error reading post {}'.format(base_path)) - raise err + raise return timeline + + def supported_extensions(self): + """Return a list of supported file extensions, or None if such a list isn't known beforehand.""" + return list({os.path.splitext(x[0])[1] for x in self.site.config['post_pages']}) diff --git a/nikola/plugins/misc/taxonomies_classifier.plugin b/nikola/plugins/misc/taxonomies_classifier.plugin new file mode 100644 index 0000000..55c59af --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_taxonomies +module = taxonomies_classifier + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Classifies the timeline into taxonomies. + +[Nikola] +PluginCategory = SignalHandler diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py new file mode 100644 index 0000000..da8045b --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the taxonomy overviews, classification pages and feeds.""" + +import functools +import os +import sys +from collections import defaultdict + +import blinker +import natsort + +from nikola.plugin_categories import SignalHandler +from nikola import utils, hierarchy_utils + + +class TaxonomiesClassifier(SignalHandler): + """Classify posts and pages by taxonomies.""" + + name = "classify_taxonomies" + + def _do_classification(self, site): + # Needed to avoid strange errors during tests + if site is not self.site: + return + + # Get list of enabled taxonomy plugins and initialize data structures + taxonomies = site.taxonomy_plugins.values() + site.posts_per_classification = {} + for taxonomy in taxonomies: + site.posts_per_classification[taxonomy.classification_name] = { + lang: defaultdict(set) for lang in site.config['TRANSLATIONS'].keys() + } + + # Classify posts + for post in site.timeline: + # Do classify pages, but don’t classify posts that are hidden + # (draft/private/future) + if post.is_post and not post.use_in_feeds: + continue + for taxonomy in taxonomies: + if taxonomy.apply_to_posts if post.is_post else taxonomy.apply_to_pages: + classifications = {} + for lang in site.config['TRANSLATIONS'].keys(): + # Extract classifications for this language + classifications[lang] = taxonomy.classify(post, lang) + if not taxonomy.more_than_one_classifications_per_post and len(classifications[lang]) > 1: + raise ValueError("Too many {0} classifications for post {1}".format(taxonomy.classification_name, post.source_path)) + # Add post to sets + for classification in classifications[lang]: + while True: + site.posts_per_classification[taxonomy.classification_name][lang][classification].add(post) + if not taxonomy.include_posts_from_subhierarchies or not taxonomy.has_hierarchy: + break + classification_path = taxonomy.extract_hierarchy(classification) + if len(classification_path) <= 1: + if len(classification_path) == 0 or not taxonomy.include_posts_into_hierarchy_root: + break + classification = taxonomy.recombine_classification_from_hierarchy(classification_path[:-1]) + + # Sort everything. + site.page_count_per_classification = {} + site.hierarchy_per_classification = {} + site.flat_hierarchy_per_classification = {} + site.hierarchy_lookup_per_classification = {} + for taxonomy in taxonomies: + site.page_count_per_classification[taxonomy.classification_name] = {} + # Sort post lists + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): + # Ensure implicit classifications are inserted + for classification in taxonomy.get_implicit_classifications(lang): + if classification not in posts_per_classification: + posts_per_classification[classification] = [] + site.page_count_per_classification[taxonomy.classification_name][lang] = {} + # Convert sets to lists and sort them + for classification in list(posts_per_classification.keys()): + posts = list(posts_per_classification[classification]) + posts = self.site.sort_posts_chronologically(posts, lang) + taxonomy.sort_posts(posts, classification, lang) + posts_per_classification[classification] = posts + # Create hierarchy information + if taxonomy.has_hierarchy: + site.hierarchy_per_classification[taxonomy.classification_name] = {} + site.flat_hierarchy_per_classification[taxonomy.classification_name] = {} + site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {} + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): + # Compose hierarchy + hierarchy = {} + for classification in posts_per_classification.keys(): + hier = taxonomy.extract_hierarchy(classification) + node = hierarchy + for he in hier: + if he not in node: + node[he] = {} + node = node[he] + hierarchy_lookup = {} + + def create_hierarchy(hierarchy, parent=None, level=0): + """Create hierarchy.""" + result = {} + for name, children in hierarchy.items(): + node = hierarchy_utils.TreeNode(name, parent) + node.children = create_hierarchy(children, node, level + 1) + node.classification_path = [pn.name for pn in node.get_path()] + node.classification_name = taxonomy.recombine_classification_from_hierarchy(node.classification_path) + hierarchy_lookup[node.classification_name] = node + result[node.name] = node + classifications = natsort.natsorted(result.keys(), alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications, lang, level=level) + return [result[classification] for classification in classifications] + + root_list = create_hierarchy(hierarchy) + if '' in posts_per_classification: + node = hierarchy_utils.TreeNode('', parent=None) + node.children = root_list + node.classification_path = [] + node.classification_name = '' + hierarchy_lookup[node.name] = node + root_list = [node] + flat_hierarchy = hierarchy_utils.flatten_tree_structure(root_list) + # Store result + site.hierarchy_per_classification[taxonomy.classification_name][lang] = root_list + site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy + site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup + taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name], + site.flat_hierarchy_per_classification[taxonomy.classification_name], + site.hierarchy_lookup_per_classification[taxonomy.classification_name]) + else: + taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name]) + + # Check for valid paths and for collisions + taxonomy_outputs = {lang: dict() for lang in site.config['TRANSLATIONS'].keys()} + quit = False + for taxonomy in taxonomies: + # Check for collisions (per language) + for lang in site.config['TRANSLATIONS'].keys(): + if not taxonomy.is_enabled(lang): + continue + for classification, posts in site.posts_per_classification[taxonomy.classification_name][lang].items(): + # Do we actually generate this classification page? + filtered_posts = [x for x in posts if self.site.config["SHOW_UNTRANSLATED_POSTS"] or x.is_translation_available(lang)] + generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang) + if not generate_list: + continue + # Obtain path as tuple + path = site.path_handlers[taxonomy.classification_name](classification, lang) + # Check that path is OK + for path_element in path: + if len(path_element) == 0: + utils.LOGGER.error("{0} {1} yields invalid path '{2}'!".format(taxonomy.classification_name.title(), classification, '/'.join(path))) + quit = True + # Combine path + path = os.path.join(*[os.path.normpath(p) for p in path if p != '.']) + # Determine collisions + if path in taxonomy_outputs[lang]: + other_classification_name, other_classification, other_posts = taxonomy_outputs[lang][path] + if other_classification_name == taxonomy.classification_name and other_classification == classification: + taxonomy_outputs[lang][path][2].extend(filtered_posts) + else: + utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {2} "{3}" both result in output path {4} for language {5}.'.format( + taxonomy.classification_name, classification, other_classification_name, other_classification, path, lang)) + utils.LOGGER.error('{0} "{1}" is used in: {2}'.format( + taxonomy.classification_name.title(), classification, ', '.join(sorted([p.source_path for p in filtered_posts])))) + utils.LOGGER.error('{0} "{1}" is used in: {2}'.format( + other_classification_name.title(), other_classification, ', '.join(sorted([p.source_path for p in other_posts])))) + quit = True + else: + taxonomy_outputs[lang][path] = (taxonomy.classification_name, classification, list(posts)) + if quit: + sys.exit(1) + blinker.signal('taxonomies_classified').send(site) + + def _get_filtered_list(self, taxonomy, classification, lang): + """Return the filtered list of posts for this classification and language.""" + post_list = self.site.posts_per_classification[taxonomy.classification_name][lang].get(classification, []) + if self.site.config["SHOW_UNTRANSLATED_POSTS"]: + return post_list + else: + return [x for x in post_list if x.is_translation_available(lang)] + + @staticmethod + def _compute_number_of_pages(filtered_posts, posts_count): + """Given a list of posts and the maximal number of posts per page, computes the number of pages needed.""" + return min(1, (len(filtered_posts) + posts_count - 1) // posts_count) + + def _postprocess_path(self, path, lang, append_index='auto', dest_type='page', page_info=None, alternative_path=False): + """Postprocess a generated path. + + Takes the path `path` for language `lang`, and postprocesses it. + + It appends `site.config['INDEX_FILE']` depending on `append_index` + (which can have the values `'always'`, `'never'` and `'auto'`) and + `site.config['PRETTY_URLS']`. + + It also modifies/adds the extension of the last path element resp. + `site.config['INDEX_FILE']` depending on `dest_type`, which can be + `'feed'`, `'rss'` or `'page'`. + + If `dest_type` is `'page'`, `page_info` can be `None` or a tuple + of two integers: the page number and the number of pages. This will + be used to append the correct page number by calling + `utils.adjust_name_for_index_path_list` and + `utils.get_displayed_page_number`. + + If `alternative_path` is set to `True`, `utils.adjust_name_for_index_path_list` + is called with `force_addition=True`, resulting in an alternative path for the + first page of an index or Atom feed by including the page number into the path. + """ + # Forcing extension for Atom feeds and RSS feeds + force_extension = None + if dest_type == 'feed': + force_extension = self.site.config['ATOM_EXTENSION'] + elif dest_type == 'rss': + force_extension = self.site.config['RSS_EXTENSION'] + # Determine how to extend path + path = [_f for _f in path if _f] + if force_extension is not None: + if len(path) == 0 and dest_type == 'rss': + path = [self.site.config['RSS_FILENAME_BASE'](lang)] + elif len(path) == 0 and dest_type == 'feed': + path = [self.site.config['ATOM_FILENAME_BASE'](lang)] + elif len(path) == 0 or append_index == 'always': + path = path + [os.path.splitext(self.site.config['INDEX_FILE'])[0]] + elif len(path) > 0 and append_index == 'never': + path[-1] = os.path.splitext(path[-1])[0] + path[-1] += force_extension + elif (self.site.config['PRETTY_URLS'] and append_index != 'never') or len(path) == 0 or append_index == 'always': + path = path + [self.site.config['INDEX_FILE']] + elif append_index != 'never': + path[-1] += '.html' + # Create path + result = [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + path if _f] + if page_info is not None and dest_type in ('page', 'feed'): + result = utils.adjust_name_for_index_path_list(result, + page_info[0], + utils.get_displayed_page_number(page_info[0], page_info[1], self.site), + lang, + self.site, force_addition=alternative_path, extension=force_extension) + return result + + @staticmethod + def _parse_path_result(result): + """Interpret the return values of taxonomy.get_path() and taxonomy.get_overview_path() as if all three return values were given.""" + if not isinstance(result[0], (list, tuple)): + # The result must be a list or tuple of strings. Wrap into a tuple + result = (result, ) + path = result[0] + append_index = result[1] if len(result) > 1 else 'auto' + page_info = result[2] if len(result) > 2 else None + return path, append_index, page_info + + def _taxonomy_index_path(self, name, lang, taxonomy): + """Return path to the classification overview.""" + result = taxonomy.get_overview_path(lang) + path, append_index, _ = self._parse_path_result(result) + return self._postprocess_path(path, lang, append_index=append_index, dest_type='list') + + def _taxonomy_path(self, name, lang, taxonomy, dest_type='page', page=None, alternative_path=False): + """Return path to a classification.""" + if taxonomy.has_hierarchy: + result = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang, dest_type=dest_type) + else: + result = taxonomy.get_path(name, lang, dest_type=dest_type) + path, append_index, page_ = self._parse_path_result(result) + + if page is not None: + page = int(page) + else: + page = page_ + + page_info = None + if taxonomy.show_list_as_index and page is not None: + number_of_pages = self.site.page_count_per_classification[taxonomy.classification_name][lang].get(name) + if number_of_pages is None: + number_of_pages = self._compute_number_of_pages(self._get_filtered_list(taxonomy, name, lang), self.site.config['INDEX_DISPLAY_POST_COUNT']) + self.site.page_count_per_classification[taxonomy.classification_name][lang][name] = number_of_pages + page_info = (page, number_of_pages) + return self._postprocess_path(path, lang, append_index=append_index, dest_type=dest_type, page_info=page_info) + + def _taxonomy_atom_path(self, name, lang, taxonomy, page=None, alternative_path=False): + """Return path to a classification Atom feed.""" + return self._taxonomy_path(name, lang, taxonomy, dest_type='feed', page=page, alternative_path=alternative_path) + + def _taxonomy_rss_path(self, name, lang, taxonomy): + """Return path to a classification RSS feed.""" + return self._taxonomy_path(name, lang, taxonomy, dest_type='rss') + + def _register_path_handlers(self, taxonomy): + functions = ( + ('{0}_index', self._taxonomy_index_path), + ('{0}', self._taxonomy_path), + ('{0}_atom', self._taxonomy_atom_path), + ('{0}_rss', self._taxonomy_rss_path), + ) + + for name, function in functions: + name = name.format(taxonomy.classification_name) + p = functools.partial(function, taxonomy=taxonomy) + doc = taxonomy.path_handler_docstrings[name] + if doc is not False: + p.__doc__ = doc + self.site.register_path_handler(name, p) + + def set_site(self, site): + """Set site, which is a Nikola instance.""" + super().set_site(site) + # Add hook for after post scanning + blinker.signal("scanned").connect(self._do_classification) + # Register path handlers + for taxonomy in site.taxonomy_plugins.values(): + self._register_path_handlers(taxonomy) diff --git a/nikola/plugins/shortcode/chart.plugin b/nikola/plugins/shortcode/chart.plugin new file mode 100644 index 0000000..edcbc13 --- /dev/null +++ b/nikola/plugins/shortcode/chart.plugin @@ -0,0 +1,13 @@ +[Core] +name = chart +module = chart + +[Nikola] +PluginCategory = Shortcode + +[Documentation] +author = Roberto Alsina +version = 0.1 +website = https://getnikola.com/ +description = Chart directive based in PyGal + diff --git a/nikola/plugins/shortcode/chart.py b/nikola/plugins/shortcode/chart.py new file mode 100644 index 0000000..64341e8 --- /dev/null +++ b/nikola/plugins/shortcode/chart.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Chart shortcode.""" + +from ast import literal_eval + +from nikola.plugin_categories import ShortcodePlugin +from nikola.utils import req_missing, load_data + +try: + import pygal +except ImportError: + pygal = None + +_site = None + + +class ChartShortcode(ShortcodePlugin): + """Plugin for chart shortcode.""" + + name = "chart" + + def handler(self, chart_type, **_options): + """Generate chart using Pygal.""" + if pygal is None: + msg = req_missing( + ['pygal'], 'use the Chart directive', optional=True) + return '<div class="text-error">{0}</div>'.format(msg) + options = {} + chart_data = [] + _options.pop('post', None) + _options.pop('site') + data = _options.pop('data') + + for line in data.splitlines(): + line = line.strip() + if line: + chart_data.append(literal_eval('({0})'.format(line))) + if 'data_file' in _options: + options = load_data(_options['data_file']) + _options.pop('data_file') + if not chart_data: # If there is data in the document, it wins + for k, v in options.pop('data', {}).items(): + chart_data.append((k, v)) + + options.update(_options) + + style_name = options.pop('style', 'BlueStyle') + if '(' in style_name: # Parametric style + style = eval('pygal.style.' + style_name) + else: + style = getattr(pygal.style, style_name) + for k, v in options.items(): + try: + options[k] = literal_eval(v) + except Exception: + options[k] = v + chart = pygal + for o in chart_type.split('.'): + chart = getattr(chart, o) + chart = chart(style=style) + if _site and _site.invariant: + chart.no_prefix = True + chart.config(**options) + for label, series in chart_data: + chart.add(label, series) + return chart.render().decode('utf8') diff --git a/nikola/plugins/shortcode/emoji.plugin b/nikola/plugins/shortcode/emoji.plugin new file mode 100644 index 0000000..c9a272c --- /dev/null +++ b/nikola/plugins/shortcode/emoji.plugin @@ -0,0 +1,13 @@ +[Core] +name = emoji +module = emoji + +[Nikola] +PluginCategory = Shortcode + +[Documentation] +author = Roberto Alsina +version = 0.1 +website = https://getnikola.com/ +description = emoji shortcode + diff --git a/nikola/plugins/shortcode/emoji/__init__.py b/nikola/plugins/shortcode/emoji/__init__.py new file mode 100644 index 0000000..9ae2228 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# This file is public domain according to its author, Roberto Alsina + +"""Emoji directive for reStructuredText.""" + +import glob +import json +import os + +from nikola.plugin_categories import ShortcodePlugin +from nikola import utils + +TABLE = {} + +LOGGER = utils.get_logger('scan_posts') + + +def _populate(): + for fname in glob.glob(os.path.join(os.path.dirname(__file__), 'data', '*.json')): + with open(fname, encoding="utf-8-sig") as inf: + data = json.load(inf) + data = data[list(data.keys())[0]] + data = data[list(data.keys())[0]] + for item in data: + if item['key'] in TABLE: + LOGGER.warning('Repeated emoji {}'.format(item['key'])) + else: + TABLE[item['key']] = item['value'] + + +class Plugin(ShortcodePlugin): + """Plugin for gist directive.""" + + name = "emoji" + + def handler(self, name, filename=None, site=None, data=None, lang=None, post=None): + """Create HTML for emoji.""" + if not TABLE: + _populate() + try: + output = u'''<span class="emoji">{}</span>'''.format(TABLE[name]) + except KeyError: + LOGGER.warning('Unknown emoji {}'.format(name)) + output = u'''<span class="emoji error">{}</span>'''.format(name) + + return output, [] diff --git a/nikola/plugins/shortcode/emoji/data/Activity.json b/nikola/plugins/shortcode/emoji/data/Activity.json new file mode 100644 index 0000000..1461f19 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Activity.json @@ -0,0 +1,418 @@ +{
+ "activities": {
+ "activity": [
+ {
+ "key": "soccer_ball",
+ "value": "⚽"
+ },
+ {
+ "key": "basket_ball",
+ "value": "🏀"
+ },
+ {
+ "key": "american_football",
+ "value": "🏈"
+ },
+ {
+ "key": "baseball",
+ "value": "⚾"
+ },
+ {
+ "key": "tennis_racquet_ball",
+ "value": "🎾"
+ },
+ {
+ "key": "volley_ball",
+ "value": "🏐"
+ },
+ {
+ "key": "rugby_football",
+ "value": "🏉"
+ },
+ {
+ "key": "billiards",
+ "value": "🎱"
+ },
+ {
+ "key": "activity_in_hole",
+ "value": "⛳"
+ },
+ {
+ "key": "golfer",
+ "value": "🏌"
+ },
+ {
+ "key": "table_tennis_paddle_ball",
+ "value": "🏓"
+ },
+ {
+ "key": "badminton_racquet_shuttle_cock",
+ "value": "🏸"
+ },
+ {
+ "key": "ice_hockey_stick_puck",
+ "value": "🏒"
+ },
+ {
+ "key": "field_hockey_stick_ball",
+ "value": "🏑"
+ },
+ {
+ "key": "cricket_bat_ball",
+ "value": "🏏"
+ },
+ {
+ "key": "ski_and_ski_boot",
+ "value": "🎿"
+ },
+ {
+ "key": "skier",
+ "value": "⛷"
+ },
+ {
+ "key": "snow_boarder",
+ "value": "🏂"
+ },
+ {
+ "key": "ice_skate",
+ "value": "⛸"
+ },
+ {
+ "key": "bow_and_arrow",
+ "value": "🏹"
+ },
+ {
+ "key": "fishing_pole_and_fish",
+ "value": "🎣"
+ },
+ {
+ "key": "row_boat",
+ "value": "🚣"
+ },
+ {
+ "key": "row_boat_type_1_2",
+ "value": "🚣🏻"
+ },
+ {
+ "key": "row_boat_type_3",
+ "value": "🚣🏼"
+ },
+ {
+ "key": "row_boat_type_4",
+ "value": "🚣🏽"
+ },
+ {
+ "key": "row_boat_type_5",
+ "value": "🚣🏾"
+ },
+ {
+ "key": "row_boat_type_6",
+ "value": "🚣🏿"
+ },
+ {
+ "key": "swimmer",
+ "value": "🏊"
+ },
+ {
+ "key": "swimmer_type_1_2",
+ "value": "🏊🏻"
+ },
+ {
+ "key": "swimmer_type_3",
+ "value": "🏊🏼"
+ },
+ {
+ "key": "swimmer_type_4",
+ "value": "🏊🏽"
+ },
+ {
+ "key": "swimmer_type_5",
+ "value": "🏊🏾"
+ },
+ {
+ "key": "swimmer_type_6",
+ "value": "🏊🏿"
+ },
+ {
+ "key": "surfer",
+ "value": "🏄"
+ },
+ {
+ "key": "surfer_type_1_2",
+ "value": "🏄🏻"
+ },
+ {
+ "key": "surfer_type_3",
+ "value": "🏄🏼"
+ },
+ {
+ "key": "surfer_type_4",
+ "value": "🏄🏽"
+ },
+ {
+ "key": "surfer_type_5",
+ "value": "🏄🏾"
+ },
+ {
+ "key": "surfer_type_6",
+ "value": "🏄🏿"
+ },
+ {
+ "key": "bath",
+ "value": "🛀"
+ },
+ {
+ "key": "bath_type_1_2",
+ "value": "🛀🏻"
+ },
+ {
+ "key": "bath_type_3",
+ "value": "🛀🏼"
+ },
+ {
+ "key": "bath_type_4",
+ "value": "🛀🏽"
+ },
+ {
+ "key": "bath_type_5",
+ "value": "🛀🏾"
+ },
+ {
+ "key": "bath_type_6",
+ "value": "🛀🏿"
+ },
+ {
+ "key": "person_with_ball",
+ "value": "⛹"
+ },
+ {
+ "key": "person_with_ball_type_1_2",
+ "value": "⛹🏻"
+ },
+ {
+ "key": "person_with_ball_type_3",
+ "value": "⛹🏼"
+ },
+ {
+ "key": "person_with_ball_type_4",
+ "value": "⛹🏽"
+ },
+ {
+ "key": "person_with_ball_type_5",
+ "value": "⛹🏾"
+ },
+ {
+ "key": "person_with_ball_type_6",
+ "value": "⛹🏿"
+ },
+ {
+ "key": "weight_lifter",
+ "value": "🏋"
+ },
+ {
+ "key": "weight_lifter_type_1_2",
+ "value": "🏋🏻"
+ },
+ {
+ "key": "weight_lifter_type_3",
+ "value": "🏋🏼"
+ },
+ {
+ "key": "weight_lifter_type_4",
+ "value": "🏋🏽"
+ },
+ {
+ "key": "weight_lifter_type_5",
+ "value": "🏋🏾"
+ },
+ {
+ "key": "weight_lifter_type_6",
+ "value": "🏋🏿"
+ },
+ {
+ "key": "bicyclist",
+ "value": "🚴"
+ },
+ {
+ "key": "bicyclist_type_1_2",
+ "value": "🚴🏻"
+ },
+ {
+ "key": "bicyclist_type_3",
+ "value": "🚴🏼"
+ },
+ {
+ "key": "bicyclist_type_4",
+ "value": "🚴🏽"
+ },
+ {
+ "key": "bicyclist_type_5",
+ "value": "🚴🏾"
+ },
+ {
+ "key": "bicyclist_type_6",
+ "value": "🚴🏿"
+ },
+ {
+ "key": "mountain_bicyclist",
+ "value": "🚵"
+ },
+ {
+ "key": "mountain_bicyclist_type_1_2",
+ "value": "🚵🏻"
+ },
+ {
+ "key": "mountain_bicyclist_type_3",
+ "value": "🚵🏼"
+ },
+ {
+ "key": "mountain_bicyclist_type_4",
+ "value": "🚵🏽"
+ },
+ {
+ "key": "mountain_bicyclist_type_5",
+ "value": "🚵🏾"
+ },
+ {
+ "key": "mountain_bicyclist_type_6",
+ "value": "🚵🏿"
+ },
+ {
+ "key": "horse_racing",
+ "value": "🏇"
+ },
+ {
+ "key": "horse_racing_type_1_2",
+ "value": "🏇🏻"
+ },
+ {
+ "key": "horse_racing_type_3",
+ "value": "🏇🏻"
+ },
+ {
+ "key": "horse_racing_type_4",
+ "value": "🏇🏽"
+ },
+ {
+ "key": "horse_racing_type_5",
+ "value": "🏇🏾"
+ },
+ {
+ "key": "horse_racing_type_6",
+ "value": "🏇🏿"
+ },
+ {
+ "key": "main_business_suit_levitating",
+ "value": "🕴"
+ },
+ {
+ "key": "trophy",
+ "value": "🏆"
+ },
+ {
+ "key": "running_shirt_with_sash",
+ "value": "🎽"
+ },
+ {
+ "key": "sports_medal",
+ "value": "🏅"
+ },
+ {
+ "key": "military_medal",
+ "value": "🎖"
+ },
+ {
+ "key": "reminder_ribbon",
+ "value": "🎗"
+ },
+ {
+ "key": "rosette",
+ "value": "🏵"
+ },
+ {
+ "key": "ticket",
+ "value": "🎫"
+ },
+ {
+ "key": "admission_tickets",
+ "value": "🎟"
+ },
+ {
+ "key": "performing_arts",
+ "value": "🎭"
+ },
+ {
+ "key": "artist_palette",
+ "value": "🎨"
+ },
+ {
+ "key": "circus_tent",
+ "value": "🎪"
+ },
+ {
+ "key": "microphone",
+ "value": "🎤"
+ },
+ {
+ "key": "headphone",
+ "value": "🎧"
+ },
+ {
+ "key": "musical_score",
+ "value": "🎼"
+ },
+ {
+ "key": "musical_keyboard",
+ "value": "🎹"
+ },
+ {
+ "key": "saxophone",
+ "value": "🎷"
+ },
+ {
+ "key": "trumpet",
+ "value": "🎺"
+ },
+ {
+ "key": "guitar",
+ "value": "🎸"
+ },
+ {
+ "key": "violin",
+ "value": "🎻"
+ },
+ {
+ "key": "clapper_board",
+ "value": "🎬"
+ },
+ {
+ "key": "video_game",
+ "value": "🎮"
+ },
+ {
+ "key": "alien_monster",
+ "value": "👾"
+ },
+ {
+ "key": "direct_hit",
+ "value": "🎯"
+ },
+ {
+ "key": "game_die",
+ "value": "🎲"
+ },
+ {
+ "key": "slot_machine",
+ "value": "🎰"
+ },
+ {
+ "key": "bowling",
+ "value": "🎳"
+ },
+ {
+ "key": "olympic_rings",
+ "value": "◯◯◯◯◯"
+ }
+ ]
+ }
+}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/Flags.json b/nikola/plugins/shortcode/emoji/data/Flags.json new file mode 100644 index 0000000..d1d4bdc --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Flags.json @@ -0,0 +1,998 @@ +{
+ "flags": {
+ "flag": [
+ {
+ "key": "afghanistan",
+ "value": "🇦🇫"
+ },
+ {
+ "key": "land_island",
+ "value": "🇦🇽"
+ },
+ {
+ "key": "albania",
+ "value": "🇦🇱"
+ },
+ {
+ "key": "algeria",
+ "value": "🇩🇿"
+ },
+ {
+ "key": "american_samoa",
+ "value": "🇦🇸"
+ },
+ {
+ "key": "andorra",
+ "value": "🇦🇩"
+ },
+ {
+ "key": "angola",
+ "value": "🇦🇴"
+ },
+ {
+ "key": "anguilla",
+ "value": "🇦🇮"
+ },
+ {
+ "key": "antarctica",
+ "value": "🇦🇶"
+ },
+ {
+ "key": "antigua_and_barbuda",
+ "value": "🇦🇬"
+ },
+ {
+ "key": "argentina",
+ "value": "🇦🇷"
+ },
+ {
+ "key": "armenia",
+ "value": "🇦🇲"
+ },
+ {
+ "key": "aruba",
+ "value": "🇦🇼"
+ },
+ {
+ "key": "australia",
+ "value": "🇦🇺"
+ },
+ {
+ "key": "austria",
+ "value": "🇦🇹"
+ },
+ {
+ "key": "azerbaijan",
+ "value": "🇦🇿"
+ },
+ {
+ "key": "bahamas",
+ "value": "🇧🇸"
+ },
+ {
+ "key": "bahrain",
+ "value": "🇧🇭"
+ },
+ {
+ "key": "bangladesh",
+ "value": "🇧🇩"
+ },
+ {
+ "key": "barbados",
+ "value": "🇧🇧"
+ },
+ {
+ "key": "belarus",
+ "value": "🇧🇾"
+ },
+ {
+ "key": "belgium",
+ "value": "🇧🇪"
+ },
+ {
+ "key": "belize",
+ "value": "🇧🇿"
+ },
+ {
+ "key": "benin",
+ "value": "🇧🇯"
+ },
+ {
+ "key": "bermuda",
+ "value": "🇧🇲"
+ },
+ {
+ "key": "bhutan",
+ "value": "🇧🇹"
+ },
+ {
+ "key": "bolivia",
+ "value": "🇧🇴"
+ },
+ {
+ "key": "caribbean_netherlands",
+ "value": "🇧🇶"
+ },
+ {
+ "key": "bosnia_and_herzegovina",
+ "value": "🇧🇦"
+ },
+ {
+ "key": "botswana",
+ "value": "🇧🇼"
+ },
+ {
+ "key": "brazil",
+ "value": "🇧🇷"
+ },
+ {
+ "key": "british_indian_ocean_territory",
+ "value": "🇮🇴"
+ },
+ {
+ "key": "british_virgin_islands",
+ "value": "🇻🇬"
+ },
+ {
+ "key": "brunei",
+ "value": "🇧🇳"
+ },
+ {
+ "key": "bulgaria",
+ "value": "🇧🇬"
+ },
+ {
+ "key": "burkina_faso",
+ "value": "🇧🇫"
+ },
+ {
+ "key": "burundi",
+ "value": "🇧🇮"
+ },
+ {
+ "key": "cape_verde",
+ "value": "🇨🇻"
+ },
+ {
+ "key": "cambodia",
+ "value": "🇰🇭"
+ },
+ {
+ "key": "cameroon",
+ "value": "🇨🇲"
+ },
+ {
+ "key": "canada",
+ "value": "🇨🇦"
+ },
+ {
+ "key": "canary_islands",
+ "value": "🇮🇨"
+ },
+ {
+ "key": "cayman_islands",
+ "value": "🇰🇾"
+ },
+ {
+ "key": "central_african_republic",
+ "value": "🇨🇫"
+ },
+ {
+ "key": "chad",
+ "value": "🇹🇩"
+ },
+ {
+ "key": "chile",
+ "value": "🇨🇱"
+ },
+ {
+ "key": "china",
+ "value": "🇨🇳"
+ },
+ {
+ "key": "christmas_island",
+ "value": "🇨🇽"
+ },
+ {
+ "key": "cocos_keeling_island",
+ "value": "🇨🇨"
+ },
+ {
+ "key": "colombia",
+ "value": "🇨🇴"
+ },
+ {
+ "key": "comoros",
+ "value": "🇰🇲"
+ },
+ {
+ "key": "congo_brazzaville",
+ "value": "🇨🇬"
+ },
+ {
+ "key": "congo_kingshasa",
+ "value": "🇨🇩"
+ },
+ {
+ "key": "cook_islands",
+ "value": "🇨🇰"
+ },
+ {
+ "key": "costa_rica",
+ "value": "🇨🇷"
+ },
+ {
+ "key": "croatia",
+ "value": "🇭🇷"
+ },
+ {
+ "key": "cuba",
+ "value": "🇨🇺"
+ },
+ {
+ "key": "curaao",
+ "value": "🇨🇼"
+ },
+ {
+ "key": "cyprus",
+ "value": "🇨🇾"
+ },
+ {
+ "key": "czech_republic",
+ "value": "🇨🇿"
+ },
+ {
+ "key": "denmark",
+ "value": "🇩🇰"
+ },
+ {
+ "key": "djibouti",
+ "value": "🇩🇯"
+ },
+ {
+ "key": "dominica",
+ "value": "🇩🇲"
+ },
+ {
+ "key": "dominican_republic",
+ "value": "🇩🇴"
+ },
+ {
+ "key": "ecuador",
+ "value": "🇪🇨"
+ },
+ {
+ "key": "egypt",
+ "value": "🇪🇬"
+ },
+ {
+ "key": "el_salvador",
+ "value": "🇸🇻"
+ },
+ {
+ "key": "equatorial_guinea",
+ "value": "🇬🇶"
+ },
+ {
+ "key": "eritrea",
+ "value": "🇪🇷"
+ },
+ {
+ "key": "estonia",
+ "value": "🇪🇪"
+ },
+ {
+ "key": "ethiopia",
+ "value": "🇪🇹"
+ },
+ {
+ "key": "european_union",
+ "value": "🇪🇺"
+ },
+ {
+ "key": "falkland_islands",
+ "value": "🇫🇰"
+ },
+ {
+ "key": "faroe_islands",
+ "value": "🇫🇴"
+ },
+ {
+ "key": "fiji",
+ "value": "🇫🇯"
+ },
+ {
+ "key": "finland",
+ "value": "🇫🇮"
+ },
+ {
+ "key": "france",
+ "value": "🇫🇷"
+ },
+ {
+ "key": "french_guiana",
+ "value": "🇬🇫"
+ },
+ {
+ "key": "french_polynesia",
+ "value": "🇵🇫"
+ },
+ {
+ "key": "french_southern_territories",
+ "value": "🇹🇫"
+ },
+ {
+ "key": "gabon",
+ "value": "🇬🇦"
+ },
+ {
+ "key": "gambia",
+ "value": "🇬🇲"
+ },
+ {
+ "key": "georgia",
+ "value": "🇬🇪"
+ },
+ {
+ "key": "germany",
+ "value": "🇩🇪"
+ },
+ {
+ "key": "ghana",
+ "value": "🇬🇭"
+ },
+ {
+ "key": "gibraltar",
+ "value": "🇬🇮"
+ },
+ {
+ "key": "greece",
+ "value": "🇬🇷"
+ },
+ {
+ "key": "greenland",
+ "value": "🇬🇱"
+ },
+ {
+ "key": "grenada",
+ "value": "🇬🇩"
+ },
+ {
+ "key": "guadeloupe",
+ "value": "🇬🇵"
+ },
+ {
+ "key": "guam",
+ "value": "🇬🇺"
+ },
+ {
+ "key": "guatemala",
+ "value": "🇬🇹"
+ },
+ {
+ "key": "guernsey",
+ "value": "🇬🇬"
+ },
+ {
+ "key": "guinea",
+ "value": "🇬🇳"
+ },
+ {
+ "key": "guinea_bissau",
+ "value": "🇬🇼"
+ },
+ {
+ "key": "guyana",
+ "value": "🇬🇾"
+ },
+ {
+ "key": "haiti",
+ "value": "🇭🇹"
+ },
+ {
+ "key": "honduras",
+ "value": "🇭🇳"
+ },
+ {
+ "key": "hong_kong",
+ "value": "🇭🇰"
+ },
+ {
+ "key": "hungary",
+ "value": "🇭🇺"
+ },
+ {
+ "key": "iceland",
+ "value": "🇮🇸"
+ },
+ {
+ "key": "india",
+ "value": "🇮🇳"
+ },
+ {
+ "key": "indonesia",
+ "value": "🇮🇩"
+ },
+ {
+ "key": "iran",
+ "value": "🇮🇷"
+ },
+ {
+ "key": "iraq",
+ "value": "🇮🇶"
+ },
+ {
+ "key": "ireland",
+ "value": "🇮🇪"
+ },
+ {
+ "key": "isle_of_man",
+ "value": "🇮🇲"
+ },
+ {
+ "key": "israel",
+ "value": "🇮🇱"
+ },
+ {
+ "key": "italy",
+ "value": "🇮🇹"
+ },
+ {
+ "key": "ctedivoire",
+ "value": "🇨🇮"
+ },
+ {
+ "key": "jamaica",
+ "value": "🇯🇲"
+ },
+ {
+ "key": "japan",
+ "value": "🇯🇵"
+ },
+ {
+ "key": "jersey",
+ "value": "🇯🇪"
+ },
+ {
+ "key": "jordan",
+ "value": "🇯🇴"
+ },
+ {
+ "key": "kazakhstan",
+ "value": "🇰🇿"
+ },
+ {
+ "key": "kenya",
+ "value": "🇰🇪"
+ },
+ {
+ "key": "kiribati",
+ "value": "🇰🇮"
+ },
+ {
+ "key": "kosovo",
+ "value": "🇽🇰"
+ },
+ {
+ "key": "kuwait",
+ "value": "🇰🇼"
+ },
+ {
+ "key": "kyrgyzstan",
+ "value": "🇰🇬"
+ },
+ {
+ "key": "laos",
+ "value": "🇱🇦"
+ },
+ {
+ "key": "latvia",
+ "value": "🇱🇻"
+ },
+ {
+ "key": "lebanon",
+ "value": "🇱🇧"
+ },
+ {
+ "key": "lesotho",
+ "value": "🇱🇸"
+ },
+ {
+ "key": "liberia",
+ "value": "🇱🇷"
+ },
+ {
+ "key": "libya",
+ "value": "🇱🇾"
+ },
+ {
+ "key": "liechtenstein",
+ "value": "🇱🇮"
+ },
+ {
+ "key": "lithuania",
+ "value": "🇱🇹"
+ },
+ {
+ "key": "luxembourg",
+ "value": "🇱🇺"
+ },
+ {
+ "key": "macau",
+ "value": "🇲🇴"
+ },
+ {
+ "key": "macedonia",
+ "value": "🇲🇰"
+ },
+ {
+ "key": "madagascar",
+ "value": "🇲🇬"
+ },
+ {
+ "key": "malawi",
+ "value": "🇲🇼"
+ },
+ {
+ "key": "malaysia",
+ "value": "🇲🇾"
+ },
+ {
+ "key": "maldives",
+ "value": "🇲🇻"
+ },
+ {
+ "key": "mali",
+ "value": "🇲🇱"
+ },
+ {
+ "key": "malta",
+ "value": "🇲🇹"
+ },
+ {
+ "key": "marshall_islands",
+ "value": "🇲🇭"
+ },
+ {
+ "key": "martinique",
+ "value": "🇲🇶"
+ },
+ {
+ "key": "mauritania",
+ "value": "🇲🇷"
+ },
+ {
+ "key": "mauritius",
+ "value": "🇲🇺"
+ },
+ {
+ "key": "mayotte",
+ "value": "🇾🇹"
+ },
+ {
+ "key": "mexico",
+ "value": "🇲🇽"
+ },
+ {
+ "key": "micronesia",
+ "value": "🇫🇲"
+ },
+ {
+ "key": "moldova",
+ "value": "🇲🇩"
+ },
+ {
+ "key": "monaco",
+ "value": "🇲🇨"
+ },
+ {
+ "key": "mongolia",
+ "value": "🇲🇳"
+ },
+ {
+ "key": "montenegro",
+ "value": "🇲🇪"
+ },
+ {
+ "key": "montserrat",
+ "value": "🇲🇸"
+ },
+ {
+ "key": "morocco",
+ "value": "🇲🇦"
+ },
+ {
+ "key": "mozambique",
+ "value": "🇲🇿"
+ },
+ {
+ "key": "myanmar_burma",
+ "value": "🇲🇲"
+ },
+ {
+ "key": "namibia",
+ "value": "🇳🇦"
+ },
+ {
+ "key": "nauru",
+ "value": "🇳🇷"
+ },
+ {
+ "key": "nepal",
+ "value": "🇳🇵"
+ },
+ {
+ "key": "netherlands",
+ "value": "🇳🇱"
+ },
+ {
+ "key": "new_caledonia",
+ "value": "🇳🇨"
+ },
+ {
+ "key": "new_zealand",
+ "value": "🇳🇿"
+ },
+ {
+ "key": "nicaragua",
+ "value": "🇳🇮"
+ },
+ {
+ "key": "niger",
+ "value": "🇳🇪"
+ },
+ {
+ "key": "nigeria",
+ "value": "🇳🇬"
+ },
+ {
+ "key": "niue",
+ "value": "🇳🇺"
+ },
+ {
+ "key": "norfolk_island",
+ "value": "🇳🇫"
+ },
+ {
+ "key": "northern_mariana_islands",
+ "value": "🇲🇵"
+ },
+ {
+ "key": "north_korea",
+ "value": "🇰🇵"
+ },
+ {
+ "key": "norway",
+ "value": "🇳🇴"
+ },
+ {
+ "key": "oman",
+ "value": "🇴🇲"
+ },
+ {
+ "key": "pakistan",
+ "value": "🇵🇰"
+ },
+ {
+ "key": "palau",
+ "value": "🇵🇼"
+ },
+ {
+ "key": "palestinian_territories",
+ "value": "🇵🇸"
+ },
+ {
+ "key": "panama",
+ "value": "🇵🇦"
+ },
+ {
+ "key": "papua_new_guinea",
+ "value": "🇵🇬"
+ },
+ {
+ "key": "paraguay",
+ "value": "🇵🇾"
+ },
+ {
+ "key": "peru",
+ "value": "🇵🇪"
+ },
+ {
+ "key": "philippines",
+ "value": "🇵🇭"
+ },
+ {
+ "key": "pitcairn_islands",
+ "value": "🇵🇳"
+ },
+ {
+ "key": "poland",
+ "value": "🇵🇱"
+ },
+ {
+ "key": "portugal",
+ "value": "🇵🇹"
+ },
+ {
+ "key": "puerto_rico",
+ "value": "🇵🇷"
+ },
+ {
+ "key": "qatar",
+ "value": "🇶🇦"
+ },
+ {
+ "key": "reunion",
+ "value": "🇷🇪"
+ },
+ {
+ "key": "romania",
+ "value": "🇷🇴"
+ },
+ {
+ "key": "russia",
+ "value": "🇷🇺"
+ },
+ {
+ "key": "rwanda",
+ "value": "🇷🇼"
+ },
+ {
+ "key": "saint_barthlemy",
+ "value": "🇧🇱"
+ },
+ {
+ "key": "saint_helena",
+ "value": "🇸🇭"
+ },
+ {
+ "key": "saint_kitts_and_nevis",
+ "value": "🇰🇳"
+ },
+ {
+ "key": "saint_lucia",
+ "value": "🇱🇨"
+ },
+ {
+ "key": "saint_pierre_and_miquelon",
+ "value": "🇵🇲"
+ },
+ {
+ "key": "st_vincent_grenadines",
+ "value": "🇻🇨"
+ },
+ {
+ "key": "samoa",
+ "value": "🇼🇸"
+ },
+ {
+ "key": "san_marino",
+ "value": "🇸🇲"
+ },
+ {
+ "key": "sotom_and_prncipe",
+ "value": "🇸🇹"
+ },
+ {
+ "key": "saudi_arabia",
+ "value": "🇸🇦"
+ },
+ {
+ "key": "senegal",
+ "value": "🇸🇳"
+ },
+ {
+ "key": "serbia",
+ "value": "🇷🇸"
+ },
+ {
+ "key": "seychelles",
+ "value": "🇸🇨"
+ },
+ {
+ "key": "sierra_leone",
+ "value": "🇸🇱"
+ },
+ {
+ "key": "singapore",
+ "value": "🇸🇬"
+ },
+ {
+ "key": "sint_maarten",
+ "value": "🇸🇽"
+ },
+ {
+ "key": "slovakia",
+ "value": "🇸🇰"
+ },
+ {
+ "key": "slovenia",
+ "value": "🇸🇮"
+ },
+ {
+ "key": "solomon_islands",
+ "value": "🇸🇧"
+ },
+ {
+ "key": "somalia",
+ "value": "🇸🇴"
+ },
+ {
+ "key": "south_africa",
+ "value": "🇿🇦"
+ },
+ {
+ "key": "south_georgia_south_sandwich_islands",
+ "value": "🇬🇸"
+ },
+ {
+ "key": "south_korea",
+ "value": "🇰🇷"
+ },
+ {
+ "key": "south_sudan",
+ "value": "🇸🇸"
+ },
+ {
+ "key": "spain",
+ "value": "🇪🇸"
+ },
+ {
+ "key": "sri_lanka",
+ "value": "🇱🇰"
+ },
+ {
+ "key": "sudan",
+ "value": "🇸🇩"
+ },
+ {
+ "key": "suriname",
+ "value": "🇸🇷"
+ },
+ {
+ "key": "swaziland",
+ "value": "🇸🇿"
+ },
+ {
+ "key": "sweden",
+ "value": "🇸🇪"
+ },
+ {
+ "key": "switzerland",
+ "value": "🇨🇭"
+ },
+ {
+ "key": "syria",
+ "value": "🇸🇾"
+ },
+ {
+ "key": "taiwan",
+ "value": "🇹🇼"
+ },
+ {
+ "key": "tajikistan",
+ "value": "🇹🇯"
+ },
+ {
+ "key": "tanzania",
+ "value": "🇹🇿"
+ },
+ {
+ "key": "thailand",
+ "value": "🇹🇭"
+ },
+ {
+ "key": "timorleste",
+ "value": "🇹🇱"
+ },
+ {
+ "key": "togo",
+ "value": "🇹🇬"
+ },
+ {
+ "key": "tokelau",
+ "value": "🇹🇰"
+ },
+ {
+ "key": "tonga",
+ "value": "🇹🇴"
+ },
+ {
+ "key": "trinidad_and_tobago",
+ "value": "🇹🇹"
+ },
+ {
+ "key": "tunisia",
+ "value": "🇹🇳"
+ },
+ {
+ "key": "turkey",
+ "value": "🇹🇷"
+ },
+ {
+ "key": "turkmenistan",
+ "value": "🇹🇲"
+ },
+ {
+ "key": "turks_and_caicos_islands",
+ "value": "🇹🇨"
+ },
+ {
+ "key": "tuvalu",
+ "value": "🇹🇻"
+ },
+ {
+ "key": "uganda",
+ "value": "🇺🇬"
+ },
+ {
+ "key": "ukraine",
+ "value": "🇺🇦"
+ },
+ {
+ "key": "united_arab_emirates",
+ "value": "🇦🇪"
+ },
+ {
+ "key": "united_kingdom",
+ "value": "🇬🇧"
+ },
+ {
+ "key": "united_states",
+ "value": "🇺🇸"
+ },
+ {
+ "key": "us_virgin_islands",
+ "value": "🇻🇮"
+ },
+ {
+ "key": "uruguay",
+ "value": "🇺🇾"
+ },
+ {
+ "key": "uzbekistan",
+ "value": "🇺🇿"
+ },
+ {
+ "key": "vanuatu",
+ "value": "🇻🇺"
+ },
+ {
+ "key": "vatican_city",
+ "value": "🇻🇦"
+ },
+ {
+ "key": "venezuela",
+ "value": "🇻🇪"
+ },
+ {
+ "key": "vietnam",
+ "value": "🇻🇳"
+ },
+ {
+ "key": "wallis_and_futuna",
+ "value": "🇼🇫"
+ },
+ {
+ "key": "western_sahara",
+ "value": "🇪🇭"
+ },
+ {
+ "key": "yemen",
+ "value": "🇾🇪"
+ },
+ {
+ "key": "zambia",
+ "value": "🇿🇲"
+ },
+ {
+ "key": "zimbabwe",
+ "value": "🇿🇼"
+ },
+ {
+ "key": "england",
+ "value": "🇽🇪"
+ }
+ ]
+ }
+}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/Food.json b/nikola/plugins/shortcode/emoji/data/Food.json new file mode 100644 index 0000000..c755a20 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Food.json @@ -0,0 +1,274 @@ +{ + "foods": { + "food": [ + { + "key": "green_apple", + "value": "🍏" + }, + { + "key": "red_apple", + "value": "🍎" + }, + { + "key": "pear", + "value": "🍐" + }, + { + "key": "tangerine", + "value": "🍊" + }, + { + "key": "lemon", + "value": "🍋" + }, + { + "key": "banana", + "value": "🍌" + }, + { + "key": "watermelon", + "value": "🍉" + }, + { + "key": "grapes", + "value": "🍇" + }, + { + "key": "strawberry", + "value": "🍓" + }, + { + "key": "melon", + "value": "🍈" + }, + { + "key": "cherry", + "value": "🍒" + }, + { + "key": "peach", + "value": "🍑" + }, + { + "key": "pineapple", + "value": "🍍" + }, + { + "key": "tomato", + "value": "🍅" + }, + { + "key": "egg_plant", + "value": "🍆" + }, + { + "key": "hot_pepper", + "value": "🌶" + }, + { + "key": "ear_of_maize", + "value": "🌽" + }, + { + "key": "roasted_sweet_potato", + "value": "🍠" + }, + { + "key": "honey_pot", + "value": "🍯" + }, + { + "key": "bread", + "value": "🍞" + }, + { + "key": "cheese", + "value": "🧀" + }, + { + "key": "poultry_leg", + "value": "🍗" + }, + { + "key": "meat_on_bone", + "value": "🍖" + }, + { + "key": "fried_shrimp", + "value": "🍤" + }, + { + "key": "cooking", + "value": "🍳" + }, + { + "key": "hamburger", + "value": "🍔" + }, + { + "key": "french_fries", + "value": "🍟" + }, + { + "key": "hot_dog", + "value": "🌭" + }, + { + "key": "slice_of_pizza", + "value": "🍕" + }, + { + "key": "spaghetti", + "value": "🍝" + }, + { + "key": "taco", + "value": "🌮" + }, + { + "key": "burrito", + "value": "🌯" + }, + { + "key": "steaming_bowl", + "value": "🍜" + }, + { + "key": "pot_of_food", + "value": "🍲" + }, + { + "key": "fish_cake", + "value": "🍥" + }, + { + "key": "sushi", + "value": "🍣" + }, + { + "key": "bento_box", + "value": "🍱" + }, + { + "key": "curry_and_rice", + "value": "🍛" + }, + { + "key": "rice_ball", + "value": "🍙" + }, + { + "key": "cooked_rice", + "value": "🍚" + }, + { + "key": "rice_cracker", + "value": "🍘" + }, + { + "key": "oden", + "value": "🍢" + }, + { + "key": "dango", + "value": "🍡" + }, + { + "key": "shaved_ice", + "value": "🍧" + }, + { + "key": "ice_cream", + "value": "🍨" + }, + { + "key": "soft_ice_cream", + "value": "🍦" + }, + { + "key": "short_cake", + "value": "🍰" + }, + { + "key": "birthday_cake", + "value": "🎂" + }, + { + "key": "custard", + "value": "🍮" + }, + { + "key": "candy", + "value": "🍬" + }, + { + "key": "lollipop", + "value": "🍭" + }, + { + "key": "chocolate_bar", + "value": "🍫" + }, + { + "key": "popcorn", + "value": "🍿" + }, + { + "key": "doughnut", + "value": "🍩" + }, + { + "key": "cookie", + "value": "🍪" + }, + { + "key": "bear_mug", + "value": "🍺" + }, + { + "key": "clinking_beer_mugs", + "value": "🍻" + }, + { + "key": "wine_glass", + "value": "🍷" + }, + { + "key": "cocktail_glass", + "value": "🍸" + }, + { + "key": "tropical_drink", + "value": "🍹" + }, + { + "key": "bottle_with_popping_cork", + "value": "🍾" + }, + { + "key": "sake_bottle_and_cup", + "value": "🍶" + }, + { + "key": "tea_cup_without_handle", + "value": "🍵" + }, + { + "key": "hot_beverage", + "value": "☕" + }, + { + "key": "baby_bottle", + "value": "🍼" + }, + { + "key": "fork_and_knife", + "value": "🍴" + }, + { + "key": "fork_and_knife_with_plate", + "value": "🍽" + } + ] + } +}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/LICENSE b/nikola/plugins/shortcode/emoji/data/LICENSE new file mode 100644 index 0000000..c7bf1f4 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/LICENSE @@ -0,0 +1,25 @@ +The MIT License (MIT) + +Copyright (c) 2016 -2017 Shayan Rais + +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. + +------------ + +Copied from https://github.com/shanraisshan/EmojiCodeSheet diff --git a/nikola/plugins/shortcode/emoji/data/Nature.json b/nikola/plugins/shortcode/emoji/data/Nature.json new file mode 100644 index 0000000..f845a64 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Nature.json @@ -0,0 +1,594 @@ +{
+ "natures": {
+ "nature": [
+ {
+ "key": "dog_face",
+ "value": "🐶"
+ },
+ {
+ "key": "cat_face",
+ "value": "🐱"
+ },
+ {
+ "key": "mouse_face",
+ "value": "🐭"
+ },
+ {
+ "key": "hamster_face",
+ "value": "🐹"
+ },
+ {
+ "key": "rabbit_face",
+ "value": "🐰"
+ },
+ {
+ "key": "bear_face",
+ "value": "🐻"
+ },
+ {
+ "key": "panda_face",
+ "value": "🐼"
+ },
+ {
+ "key": "koala_face",
+ "value": "🐨"
+ },
+ {
+ "key": "lion_face",
+ "value": "🦁"
+ },
+ {
+ "key": "cow_face",
+ "value": "🐮"
+ },
+ {
+ "key": "pig_face",
+ "value": "🐷"
+ },
+ {
+ "key": "pig_nose",
+ "value": "🐽"
+ },
+ {
+ "key": "frog_face",
+ "value": "🐸"
+ },
+ {
+ "key": "octopus",
+ "value": "🐙"
+ },
+ {
+ "key": "monkey_face",
+ "value": "🐵"
+ },
+ {
+ "key": "tiger_face",
+ "value": "🐯"
+ },
+ {
+ "key": "see_no_evil_monkey",
+ "value": "🙈"
+ },
+ {
+ "key": "hear_no_evil_monkey",
+ "value": "🙉"
+ },
+ {
+ "key": "speak_no_evil_monkey",
+ "value": "🙊"
+ },
+ {
+ "key": "monkey",
+ "value": "🐒"
+ },
+ {
+ "key": "chicken",
+ "value": "🐔"
+ },
+ {
+ "key": "penguin",
+ "value": "🐧"
+ },
+ {
+ "key": "bird",
+ "value": "🐦"
+ },
+ {
+ "key": "baby_chick",
+ "value": "🐤"
+ },
+ {
+ "key": "hatching_chick",
+ "value": "🐣"
+ },
+ {
+ "key": "front_face_chick",
+ "value": "🐥"
+ },
+ {
+ "key": "wolf_face",
+ "value": "🐺"
+ },
+ {
+ "key": "boar",
+ "value": "🐗"
+ },
+ {
+ "key": "horse_face",
+ "value": "🐴"
+ },
+ {
+ "key": "unicorn_face",
+ "value": "🦄"
+ },
+ {
+ "key": "honey_bee",
+ "value": "🐝"
+ },
+ {
+ "key": "bug",
+ "value": "🐛"
+ },
+ {
+ "key": "snail",
+ "value": "🐌"
+ },
+ {
+ "key": "lady_beetle",
+ "value": "🐞"
+ },
+ {
+ "key": "ant",
+ "value": "🐜"
+ },
+ {
+ "key": "spider",
+ "value": "🕷"
+ },
+ {
+ "key": "scorpion",
+ "value": "🦂"
+ },
+ {
+ "key": "crab",
+ "value": "🦀"
+ },
+ {
+ "key": "snake",
+ "value": "🐍"
+ },
+ {
+ "key": "turtle",
+ "value": "🐢"
+ },
+ {
+ "key": "tropical_fish",
+ "value": "🐠"
+ },
+ {
+ "key": "fish",
+ "value": "🐟"
+ },
+ {
+ "key": "blow_fish",
+ "value": "🐡"
+ },
+ {
+ "key": "dolphin",
+ "value": "🐬"
+ },
+ {
+ "key": "spouting_whale",
+ "value": "🐳"
+ },
+ {
+ "key": "whale",
+ "value": "🐋"
+ },
+ {
+ "key": "crocodile",
+ "value": "🐊"
+ },
+ {
+ "key": "leopard",
+ "value": "🐆"
+ },
+ {
+ "key": "tiger",
+ "value": "🐅"
+ },
+ {
+ "key": "water_buffalo",
+ "value": "🐃"
+ },
+ {
+ "key": "ox",
+ "value": "🐂"
+ },
+ {
+ "key": "cow",
+ "value": "🐄"
+ },
+ {
+ "key": "dromedary_camel",
+ "value": "🐪"
+ },
+ {
+ "key": "bactrian_camel",
+ "value": "🐫"
+ },
+ {
+ "key": "elephant",
+ "value": "🐘"
+ },
+ {
+ "key": "goat",
+ "value": "🐐"
+ },
+ {
+ "key": "ram",
+ "value": "🐏"
+ },
+ {
+ "key": "sheep",
+ "value": "🐑"
+ },
+ {
+ "key": "horse",
+ "value": "🐎"
+ },
+ {
+ "key": "pig",
+ "value": "🐖"
+ },
+ {
+ "key": "rat",
+ "value": "🐀"
+ },
+ {
+ "key": "mouse",
+ "value": "🐁"
+ },
+ {
+ "key": "rooster",
+ "value": "🐓"
+ },
+ {
+ "key": "turkey",
+ "value": "🦃"
+ },
+ {
+ "key": "dove",
+ "value": "🕊"
+ },
+ {
+ "key": "dog",
+ "value": "🐕"
+ },
+ {
+ "key": "poodle",
+ "value": "🐩"
+ },
+ {
+ "key": "cat",
+ "value": "🐈"
+ },
+ {
+ "key": "rabbit",
+ "value": "🐇"
+ },
+ {
+ "key": "chipmunk",
+ "value": "🐿"
+ },
+ {
+ "key": "paw_prints",
+ "value": "🐾"
+ },
+ {
+ "key": "dragon",
+ "value": "🐉"
+ },
+ {
+ "key": "dragon_face",
+ "value": "🐲"
+ },
+ {
+ "key": "cactus",
+ "value": "🌵"
+ },
+ {
+ "key": "christmas_tree",
+ "value": "🎄"
+ },
+ {
+ "key": "ever_green_tree",
+ "value": "🌲"
+ },
+ {
+ "key": "deciduous_tree",
+ "value": "🌳"
+ },
+ {
+ "key": "palm_tree",
+ "value": "🌴"
+ },
+ {
+ "key": "seedling",
+ "value": "🌱"
+ },
+ {
+ "key": "herb",
+ "value": "🌿"
+ },
+ {
+ "key": "shamrock",
+ "value": "☘"
+ },
+ {
+ "key": "four_leaf",
+ "value": "🍀"
+ },
+ {
+ "key": "pine_decoration",
+ "value": "🎍"
+ },
+ {
+ "key": "tanabata_tree",
+ "value": "🎋"
+ },
+ {
+ "key": "leaf_wind",
+ "value": "🍃"
+ },
+ {
+ "key": "fallen_leaf",
+ "value": "🍂"
+ },
+ {
+ "key": "maple_leaf",
+ "value": "🍁"
+ },
+ {
+ "key": "ear_of_rice",
+ "value": "🌾"
+ },
+ {
+ "key": "hibiscus",
+ "value": "🌺"
+ },
+ {
+ "key": "sunflower",
+ "value": "🌻"
+ },
+ {
+ "key": "rose",
+ "value": "🌹"
+ },
+ {
+ "key": "tulip",
+ "value": "🌷"
+ },
+ {
+ "key": "blossom",
+ "value": "🌼"
+ },
+ {
+ "key": "cherry_blossom",
+ "value": "🌸"
+ },
+ {
+ "key": "bouquet",
+ "value": "💐"
+ },
+ {
+ "key": "mushroom",
+ "value": "🍄"
+ },
+ {
+ "key": "chestnut",
+ "value": "🌰"
+ },
+ {
+ "key": "jack_o_lantern",
+ "value": "🎃"
+ },
+ {
+ "key": "spiral_shell",
+ "value": "🐚"
+ },
+ {
+ "key": "spider_web",
+ "value": "🕸"
+ },
+ {
+ "key": "earth_america",
+ "value": "🌎"
+ },
+ {
+ "key": "earth_europe",
+ "value": "🌍"
+ },
+ {
+ "key": "earth_australia",
+ "value": "🌏"
+ },
+ {
+ "key": "full_moon",
+ "value": "🌕"
+ },
+ {
+ "key": "waning_gibbous_moon",
+ "value": "🌖"
+ },
+ {
+ "key": "last_quarter_moon",
+ "value": "🌗"
+ },
+ {
+ "key": "waning_crescent_moon",
+ "value": "🌘"
+ },
+ {
+ "key": "new_moon_symbol",
+ "value": "🌑"
+ },
+ {
+ "key": "waxing_crescent_moon",
+ "value": "🌒"
+ },
+ {
+ "key": "first_quarter_moon",
+ "value": "🌓"
+ },
+ {
+ "key": "waxing_gibbous_moon",
+ "value": "🌔"
+ },
+ {
+ "key": "new_moon_with_face",
+ "value": "🌚"
+ },
+ {
+ "key": "full_moon_face",
+ "value": "🌝"
+ },
+ {
+ "key": "first_quarter_moon_face",
+ "value": "🌛"
+ },
+ {
+ "key": "last_quarter_moon_face",
+ "value": "🌜"
+ },
+ {
+ "key": "sun_face",
+ "value": "🌞"
+ },
+ {
+ "key": "crescent_moon",
+ "value": "🌙"
+ },
+ {
+ "key": "white_star",
+ "value": "⭐"
+ },
+ {
+ "key": "glowing_star",
+ "value": "🌟"
+ },
+ {
+ "key": "dizzy_symbol",
+ "value": "💫"
+ },
+ {
+ "key": "sparkles",
+ "value": "✨"
+ },
+ {
+ "key": "comet",
+ "value": "☄"
+ },
+ {
+ "key": "black_sun_with_rays",
+ "value": "☀"
+ },
+ {
+ "key": "white_sun_small_cloud",
+ "value": "🌤"
+ },
+ {
+ "key": "sun_behind_cloud",
+ "value": "⛅"
+ },
+ {
+ "key": "white_sun_behind_cloud",
+ "value": "🌥"
+ },
+ {
+ "key": "white_sun_behind_cloud_rain",
+ "value": "🌦"
+ },
+ {
+ "key": "cloud",
+ "value": "☁"
+ },
+ {
+ "key": "cloud_with_rain",
+ "value": "🌧"
+ },
+ {
+ "key": "thunder_cloud_rain",
+ "value": "⛈"
+ },
+ {
+ "key": "cloud_lightening",
+ "value": "🌩"
+ },
+ {
+ "key": "high_voltage",
+ "value": "⚡"
+ },
+ {
+ "key": "fire",
+ "value": "🔥"
+ },
+ {
+ "key": "collision",
+ "value": "💥"
+ },
+ {
+ "key": "snow_flake",
+ "value": "❄"
+ },
+ {
+ "key": "cloud_with_snow",
+ "value": "🌨"
+ },
+ {
+ "key": "snowman",
+ "value": "☃"
+ },
+ {
+ "key": "snowman_without_snow",
+ "value": "⛄"
+ },
+ {
+ "key": "wind_blowing_face",
+ "value": "🌬"
+ },
+ {
+ "key": "dash_symbol",
+ "value": "💨"
+ },
+ {
+ "key": "cloud_with_tornado",
+ "value": "🌪"
+ },
+ {
+ "key": "fog",
+ "value": "🌫"
+ },
+ {
+ "key": "umbrella",
+ "value": "☂"
+ },
+ {
+ "key": "umbrella_with_rain_drops",
+ "value": "☔"
+ },
+ {
+ "key": "droplet",
+ "value": "💧"
+ },
+ {
+ "key": "splashing_sweat",
+ "value": "💦"
+ },
+ {
+ "key": "water_wave",
+ "value": "🌊"
+ }
+ ]
+ }
+}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/Objects.json b/nikola/plugins/shortcode/emoji/data/Objects.json new file mode 100644 index 0000000..5f13056 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Objects.json @@ -0,0 +1,718 @@ +{ + "objects": { + "object": [ + { + "key": "watch", + "value": "⌚" + }, + { + "key": "mobile_phone", + "value": "📱" + }, + { + "key": "mobile_phone_with_right_arrow", + "value": "📲" + }, + { + "key": "personal_computer", + "value": "💻" + }, + { + "key": "keyboard", + "value": "⌨" + }, + { + "key": "desktop_computer", + "value": "🖥" + }, + { + "key": "printer", + "value": "🖨" + }, + { + "key": "three_button_mouse", + "value": "🖱" + }, + { + "key": "track_ball", + "value": "🖲" + }, + { + "key": "joystick", + "value": "🕹" + }, + { + "key": "compression", + "value": "🗜" + }, + { + "key": "mini_disc", + "value": "💽" + }, + { + "key": "floppy_disk", + "value": "💾" + }, + { + "key": "optical_disc", + "value": "💿" + }, + { + "key": "dvd", + "value": "📀" + }, + { + "key": "video_cassette", + "value": "📼" + }, + { + "key": "camera", + "value": "📷" + }, + { + "key": "camera_with_flash", + "value": "📸" + }, + { + "key": "video_camera", + "value": "📹" + }, + { + "key": "movie_camera", + "value": "🎥" + }, + { + "key": "film_projector", + "value": "📽" + }, + { + "key": "film_frames", + "value": "🎞" + }, + { + "key": "telephone_receiver", + "value": "📞" + }, + { + "key": "black_telephone", + "value": "☎" + }, + { + "key": "pager", + "value": "📟" + }, + { + "key": "fax_machine", + "value": "📠" + }, + { + "key": "television", + "value": "📺" + }, + { + "key": "radio", + "value": "📻" + }, + { + "key": "studio_microphone", + "value": "🎙" + }, + { + "key": "level_slider", + "value": "🎚" + }, + { + "key": "control_knobs", + "value": "🎛" + }, + { + "key": "stop_watch", + "value": "⏱" + }, + { + "key": "timer_clock", + "value": "⏲" + }, + { + "key": "alarm_clock", + "value": "⏰" + }, + { + "key": "mantel_piece_clock", + "value": "🕰" + }, + { + "key": "hour_glass_with_flowing_stand", + "value": "⏳" + }, + { + "key": "hour_glass", + "value": "⌛" + }, + { + "key": "satellite_antenna", + "value": "📡" + }, + { + "key": "battery", + "value": "🔋" + }, + { + "key": "electric_plug", + "value": "🔌" + }, + { + "key": "electric_light_bulb", + "value": "💡" + }, + { + "key": "electric_torch", + "value": "🔦" + }, + { + "key": "candle", + "value": "🕯" + }, + { + "key": "waste_basket", + "value": "🗑" + }, + { + "key": "oil_drum", + "value": "🛢" + }, + { + "key": "money_with_wings", + "value": "💸" + }, + { + "key": "bank_note_with_dollar_sign", + "value": "💵" + }, + { + "key": "bank_note_with_yen_sign", + "value": "💴" + }, + { + "key": "bank_note_with_euro_sign", + "value": "💶" + }, + { + "key": "bank_note_with_pounds_sign", + "value": "💷" + }, + { + "key": "money_bag", + "value": "💰" + }, + { + "key": "credit_card", + "value": "💳" + }, + { + "key": "gem_stone", + "value": "💎" + }, + { + "key": "scales", + "value": "⚖" + }, + { + "key": "wrench", + "value": "🔧" + }, + { + "key": "hammer", + "value": "🔨" + }, + { + "key": "hammer_and_pick", + "value": "⚒" + }, + { + "key": "hammer_and_wrench", + "value": "🛠" + }, + { + "key": "pick", + "value": "⛏" + }, + { + "key": "nut_and_bolt", + "value": "🔩" + }, + { + "key": "gear", + "value": "⚙" + }, + { + "key": "chains", + "value": "⛓" + }, + { + "key": "pistol", + "value": "🔫" + }, + { + "key": "bomb", + "value": "💣" + }, + { + "key": "hocho", + "value": "🔪" + }, + { + "key": "dagger_knife", + "value": "🗡" + }, + { + "key": "crossed_words", + "value": "⚔" + }, + { + "key": "shield", + "value": "🛡" + }, + { + "key": "smoking_symbol", + "value": "🚬" + }, + { + "key": "skull_and_cross_bones", + "value": "☠" + }, + { + "key": "coffin", + "value": "⚰" + }, + { + "key": "funeral_urn", + "value": "⚱" + }, + { + "key": "amphora", + "value": "🏺" + }, + { + "key": "crystal_ball", + "value": "🔮" + }, + { + "key": "prayer_beads", + "value": "📿" + }, + { + "key": "barber_pole", + "value": "💈" + }, + { + "key": "alembic", + "value": "⚗" + }, + { + "key": "telescope", + "value": "🔭" + }, + { + "key": "microscope", + "value": "🔬" + }, + { + "key": "hole", + "value": "🕳" + }, + { + "key": "pill", + "value": "💊" + }, + { + "key": "syringe", + "value": "💉" + }, + { + "key": "thermometer", + "value": "🌡" + }, + { + "key": "label", + "value": "🏷" + }, + { + "key": "bookmark", + "value": "🔖" + }, + { + "key": "toilet", + "value": "🚽" + }, + { + "key": "shower", + "value": "🚿" + }, + { + "key": "bath_tub", + "value": "🛁" + }, + { + "key": "key", + "value": "🔑" + }, + { + "key": "old_key", + "value": "🗝" + }, + { + "key": "couch_and_lamp", + "value": "🛋" + }, + { + "key": "sleeping_accommodation", + "value": "🛌" + }, + { + "key": "bed", + "value": "🛏" + }, + { + "key": "door", + "value": "🚪" + }, + { + "key": "bell_hop_bell", + "value": "🛎" + }, + { + "key": "frame_with_picture", + "value": "🖼" + }, + { + "key": "world_map", + "value": "🗺" + }, + { + "key": "umbrella_on_ground", + "value": "⛱" + }, + { + "key": "moyai", + "value": "🗿" + }, + { + "key": "shopping_bags", + "value": "🛍" + }, + { + "key": "balloon", + "value": "🎈" + }, + { + "key": "carp_streamer", + "value": "🎏" + }, + { + "key": "ribbon", + "value": "🎀" + }, + { + "key": "wrapped_present", + "value": "🎁" + }, + { + "key": "confetti_ball", + "value": "🎊" + }, + { + "key": "party_popper", + "value": "🎉" + }, + { + "key": "japanese_dolls", + "value": "🎎" + }, + { + "key": "wind_chime", + "value": "🎐" + }, + { + "key": "crossed_flags", + "value": "🎌" + }, + { + "key": "izakaya_lantern", + "value": "🏮" + }, + { + "key": "envelope", + "value": "✉" + }, + { + "key": "envelope_with_down_arrow", + "value": "📩" + }, + { + "key": "incoming_envelope", + "value": "📨" + }, + { + "key": "email_symbol", + "value": "📧" + }, + { + "key": "love_letter", + "value": "💌" + }, + { + "key": "post_box", + "value": "📮" + }, + { + "key": "closed_mail_box_with_lowered_flag", + "value": "📪" + }, + { + "key": "closed_mail_box_with_raised_flag", + "value": "📫" + }, + { + "key": "open_mail_box_with_raised_flag", + "value": "📬" + }, + { + "key": "open_mail_box_with_lowered_flag", + "value": "📭" + }, + { + "key": "package", + "value": "📦" + }, + { + "key": "postal_horn", + "value": "📯" + }, + { + "key": "inbox_tray", + "value": "📥" + }, + { + "key": "outbox_tray", + "value": "📤" + }, + { + "key": "scroll", + "value": "📜" + }, + { + "key": "page_with_curl", + "value": "📃" + }, + { + "key": "bookmark_tabs", + "value": "📑" + }, + { + "key": "bar_chart", + "value": "📊" + }, + { + "key": "chart_with_upwards_trend", + "value": "📈" + }, + { + "key": "chart_with_downwards_trend", + "value": "📉" + }, + { + "key": "page_facing_up", + "value": "📄" + }, + { + "key": "calender", + "value": "📅" + }, + { + "key": "tear_off_calendar", + "value": "📆" + }, + { + "key": "spiral_calendar_pad", + "value": "🗓" + }, + { + "key": "card_index", + "value": "📇" + }, + { + "key": "card_file_box", + "value": "🗃" + }, + { + "key": "ballot_box_with_ballot", + "value": "🗳" + }, + { + "key": "file_cabinet", + "value": "🗄" + }, + { + "key": "clip_board", + "value": "📋" + }, + { + "key": "spiral_notepad", + "value": "🗒" + }, + { + "key": "file_folder", + "value": "📁" + }, + { + "key": "open_file_folder", + "value": "📂" + }, + { + "key": "card_index_dividers", + "value": "🗂" + }, + { + "key": "rolled_up_newspaper", + "value": "🗞" + }, + { + "key": "newspaper", + "value": "📰" + }, + { + "key": "notebook", + "value": "📓" + }, + { + "key": "closed_book", + "value": "📕" + }, + { + "key": "green_book", + "value": "📗" + }, + { + "key": "blue_book", + "value": "📘" + }, + { + "key": "orange_book", + "value": "📙" + }, + { + "key": "notebook_with_decorative_cover", + "value": "📔" + }, + { + "key": "ledger", + "value": "📒" + }, + { + "key": "books", + "value": "📚" + }, + { + "key": "open_book", + "value": "📖" + }, + { + "key": "link_symbol", + "value": "🔗" + }, + { + "key": "paper_clip", + "value": "📎" + }, + { + "key": "linked_paper_clips", + "value": "🖇" + }, + { + "key": "black_scissors", + "value": "✂" + }, + { + "key": "triangular_ruler", + "value": "📐" + }, + { + "key": "straight_ruler", + "value": "📏" + }, + { + "key": "pushpin", + "value": "📌" + }, + { + "key": "round_pushpin", + "value": "📍" + }, + { + "key": "triangular_flag_post", + "value": "🚩" + }, + { + "key": "waving_white_flag", + "value": "🏳" + }, + { + "key": "waving_black_flag", + "value": "🏴" + }, + { + "key": "closed_lock_with_key", + "value": "🔐" + }, + { + "key": "lock", + "value": "🔒" + }, + { + "key": "open_lock", + "value": "🔓" + }, + { + "key": "lock_with_ink_pen", + "value": "🔏" + }, + { + "key": "lower_left_ball_point_pen", + "value": "🖊" + }, + { + "key": "lower_left_fountain_pen", + "value": "🖋" + }, + { + "key": "black_nib", + "value": "✒" + }, + { + "key": "memo", + "value": "📝" + }, + { + "key": "pencil", + "value": "✏" + }, + { + "key": "lower_left_crayon", + "value": "🖍" + }, + { + "key": "lower_left_paint_brush", + "value": "🖌" + }, + { + "key": "left_pointing_magnifying_glass", + "value": "🔍" + }, + { + "key": "right_pointing_magnifying_glass", + "value": "🔎" + } + ] + } +}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/People.json b/nikola/plugins/shortcode/emoji/data/People.json new file mode 100644 index 0000000..a5fb88f --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/People.json @@ -0,0 +1,1922 @@ +{ + "peoples": { + "people": [ + { + "key": "grinning_face", + "value": "😀" + }, + { + "key": "grimacing_face", + "value": "😬" + }, + { + "key": "grimacing_face_with_smile_eyes", + "value": "😁" + }, + { + "key": "face_with_tear_of_joy", + "value": "😂" + }, + { + "key": "smiling_face_with_open_mouth", + "value": "😃" + }, + { + "key": "smiling_face_with_open_mouth_eyes", + "value": "😄" + }, + { + "key": "smiling_face_with_open_mouth_cold_sweat", + "value": "😅" + }, + { + "key": "smiling_face_with_open_mouth_hand_tight", + "value": "😆" + }, + { + "key": "smiling_face_with_halo", + "value": "😇" + }, + { + "key": "winking_face", + "value": "😉" + }, + { + "key": "black_smiling_face", + "value": "😊" + }, + { + "key": "slightly_smiling_face", + "value": "🙂" + }, + { + "key": "upside_down_face", + "value": "🙃" + }, + { + "key": "white_smiling_face", + "value": "☺" + }, + { + "key": "face_savouring_delicious_food", + "value": "😋" + }, + { + "key": "relieved_face", + "value": "😌" + }, + { + "key": "smiling_face_heart_eyes", + "value": "😍" + }, + { + "key": "face_throwing_kiss", + "value": "😘" + }, + { + "key": "kissing_face", + "value": "😗" + }, + { + "key": "kissing_face_with_smile_eyes", + "value": "😙" + }, + { + "key": "kissing_face_with_closed_eyes", + "value": "😚" + }, + { + "key": "face_with_tongue_wink_eye", + "value": "😜" + }, + { + "key": "face_with_tongue_closed_eye", + "value": "😝" + }, + { + "key": "face_with_stuck_out_tongue", + "value": "😛" + }, + { + "key": "money_mouth_face", + "value": "🤑" + }, + { + "key": "nerd_face", + "value": "🤓" + }, + { + "key": "smiling_face_with_sun_glass", + "value": "😎" + }, + { + "key": "hugging_face", + "value": "🤗" + }, + { + "key": "smirking_face", + "value": "😏" + }, + { + "key": "face_without_mouth", + "value": "😶" + }, + { + "key": "neutral_face", + "value": "😐" + }, + { + "key": "expressionless_face", + "value": "😑" + }, + { + "key": "unamused_face", + "value": "😒" + }, + { + "key": "face_with_rolling_eyes", + "value": "🙄" + }, + { + "key": "thinking_face", + "value": "🤔" + }, + { + "key": "flushed_face", + "value": "😳" + }, + { + "key": "disappointed_face", + "value": "😞" + }, + { + "key": "worried_face", + "value": "😟" + }, + { + "key": "angry_face", + "value": "😠" + }, + { + "key": "pouting_face", + "value": "😡" + }, + { + "key": "pensive_face", + "value": "😔" + }, + { + "key": "confused_face", + "value": "😕" + }, + { + "key": "slightly_frowning_face", + "value": "🙁" + }, + { + "key": "white_frowning_face", + "value": "☹" + }, + { + "key": "persevering_face", + "value": "😣" + }, + { + "key": "confounded_face", + "value": "😖" + }, + { + "key": "tired_face", + "value": "😫" + }, + { + "key": "weary_face", + "value": "😩" + }, + { + "key": "face_with_look_of_triumph", + "value": "😤" + }, + { + "key": "face_with_open_mouth", + "value": "😮" + }, + { + "key": "face_screaming_in_fear", + "value": "😱" + }, + { + "key": "fearful_face", + "value": "😨" + }, + { + "key": "face_with_open_mouth_cold_sweat", + "value": "😰" + }, + { + "key": "hushed_face", + "value": "😯" + }, + { + "key": "frowning_face_with_open_mouth", + "value": "😦" + }, + { + "key": "anguished_face", + "value": "😧" + }, + { + "key": "crying_face", + "value": "😢" + }, + { + "key": "disappointed_but_relieved_face", + "value": "😥" + }, + { + "key": "sleepy_face", + "value": "😪" + }, + { + "key": "face_with_cold_sweat", + "value": "😓" + }, + { + "key": "loudly_crying_face", + "value": "😭" + }, + { + "key": "dizzy_face", + "value": "😵" + }, + { + "key": "astonished_face", + "value": "😲" + }, + { + "key": "zipper_mouth_face", + "value": "🤐" + }, + { + "key": "face_with_medical_mask", + "value": "😷" + }, + { + "key": "face_with_thermometer", + "value": "🤒" + }, + { + "key": "face_with_head_bandage", + "value": "🤕" + }, + { + "key": "sleeping_face", + "value": "😴" + }, + { + "key": "sleeping_symbol", + "value": "💤" + }, + { + "key": "pile_of_poo", + "value": "💩" + }, + { + "key": "smiling_face_with_horns", + "value": "😈" + }, + { + "key": "imp", + "value": "👿" + }, + { + "key": "japanese_ogre", + "value": "👹" + }, + { + "key": "japanese_goblin", + "value": "👺" + }, + { + "key": "skull", + "value": "💀" + }, + { + "key": "ghost", + "value": "👻" + }, + { + "key": "extra_terrestrial_alien", + "value": "👽" + }, + { + "key": "robot_face", + "value": "🤖" + }, + { + "key": "smiling_cat_face_open_mouth", + "value": "😺" + }, + { + "key": "grinning_cat_face_smile_eyes", + "value": "😸" + }, + { + "key": "cat_face_tears_of_joy", + "value": "😹" + }, + { + "key": "smiling_cat_face_heart_shaped_eyes", + "value": "😻" + }, + { + "key": "cat_face_wry_smile", + "value": "😼" + }, + { + "key": "kissing_cat_face_closed_eyes", + "value": "😽" + }, + { + "key": "weary_cat_face", + "value": "🙀" + }, + { + "key": "crying_cat_face", + "value": "😿" + }, + { + "key": "pouting_cat_face", + "value": "😾" + }, + { + "key": "person_both_hand_celebration", + "value": "🙌" + }, + { + "key": "person_both_hand_celebration_type_1_2", + "value": "🙌🏻" + }, + { + "key": "person_both_hand_celebration_type_3", + "value": "🙌🏼" + }, + { + "key": "person_both_hand_celebration_type_4", + "value": "🙌🏽" + }, + { + "key": "person_both_hand_celebration_type_5", + "value": "🙌🏾" + }, + { + "key": "person_both_hand_celebration_type_6", + "value": "🙌🏿" + }, + { + "key": "clapping_hand", + "value": "👏" + }, + { + "key": "clapping_hand_type_1_2", + "value": "👏🏼" + }, + { + "key": "clapping_hand_type_3", + "value": "👏🏼" + }, + { + "key": "clapping_hand_type_4", + "value": "👏🏽" + }, + { + "key": "clapping_hand_type_5", + "value": "👏🏾" + }, + { + "key": "clapping_hand_type_6", + "value": "👏🏿" + }, + { + "key": "waving_hands", + "value": "👋" + }, + { + "key": "waving_hands_type_1_2", + "value": "👋🏻" + }, + { + "key": "waving_hands_type_3", + "value": "👋🏼" + }, + { + "key": "waving_hands_type_4", + "value": "👋🏽" + }, + { + "key": "waving_hands_type_5", + "value": "👋🏾" + }, + { + "key": "waving_hands_type_6", + "value": "👋🏿" + }, + { + "key": "thumbs_up", + "value": "👍" + }, + { + "key": "thumbs_up_type_1_2", + "value": "👍🏻" + }, + { + "key": "thumbs_up_type_3", + "value": "👍🏼" + }, + { + "key": "thumbs_up_type_4", + "value": "👍🏽" + }, + { + "key": "thumbs_up_type_5", + "value": "👍🏾" + }, + { + "key": "thumbs_up_type_6", + "value": "👍🏿" + }, + { + "key": "thumbs_down", + "value": "👎" + }, + { + "key": "thumbs_down_type_1_2", + "value": "👎🏻" + }, + { + "key": "thumbs_down_type_3", + "value": "👎🏼" + }, + { + "key": "thumbs_down_type_4", + "value": "👎🏽" + }, + { + "key": "thumbs_down_type_5", + "value": "👎🏾" + }, + { + "key": "thumbs_down_type_6", + "value": "👎🏿" + }, + { + "key": "fist_hand", + "value": "👊" + }, + { + "key": "fist_hand_type_1_2", + "value": "👊🏻" + }, + { + "key": "fist_hand_type_3", + "value": "👊🏼" + }, + { + "key": "fist_hand_type_4", + "value": "👊🏽" + }, + { + "key": "fist_hand_type_5", + "value": "👊🏾" + }, + { + "key": "fist_hand_type_6", + "value": "👊🏿" + }, + { + "key": "raised_fist", + "value": "✊" + }, + { + "key": "raised_fist_type_1_2", + "value": "✊🏻" + }, + { + "key": "raised_fist_type_3", + "value": "✊🏼" + }, + { + "key": "raised_fist_type_4", + "value": "✊🏽" + }, + { + "key": "raised_fist_type_5", + "value": "✊🏾" + }, + { + "key": "raised_fist_type_6", + "value": "✊🏿" + }, + { + "key": "victory_hand", + "value": "✌" + }, + { + "key": "victory_hand_type_1_2", + "value": "✌🏻" + }, + { + "key": "victory_hand_type_3", + "value": "✌🏼" + }, + { + "key": "victory_hand_type_4", + "value": "✌🏽" + }, + { + "key": "victory_hand_type_5", + "value": "✌🏾" + }, + { + "key": "victory_hand_type_6", + "value": "✌🏿" + }, + { + "key": "ok_hand", + "value": "👌" + }, + { + "key": "ok_hand_type_1_2", + "value": "👌🏻" + }, + { + "key": "ok_hand_type_3", + "value": "👌🏼" + }, + { + "key": "ok_hand_type_4", + "value": "👌🏽" + }, + { + "key": "ok_hand_type_5", + "value": "👌🏾" + }, + { + "key": "ok_hand_type_6", + "value": "👌🏿" + }, + { + "key": "raised_hand", + "value": "✋" + }, + { + "key": "raised_hand_type_1_2", + "value": "✋🏻" + }, + { + "key": "raised_hand_type_3", + "value": "✋🏼" + }, + { + "key": "raised_hand_type_4", + "value": "✋🏽" + }, + { + "key": "raised_hand_type_5", + "value": "✋🏾" + }, + { + "key": "raised_hand_type_6", + "value": "✋🏿" + }, + { + "key": "open_hand", + "value": "👐" + }, + { + "key": "open_hand_type_1_2", + "value": "👐🏻" + }, + { + "key": "open_hand_type_3", + "value": "👐🏼" + }, + { + "key": "open_hand_type_4", + "value": "👐🏽" + }, + { + "key": "open_hand_type_5", + "value": "👐🏾" + }, + { + "key": "open_hand_type_6", + "value": "👐🏿" + }, + { + "key": "flexed_biceps", + "value": "💪" + }, + { + "key": "flexed_biceps_type_1_2", + "value": "💪🏻" + }, + { + "key": "flexed_biceps_type_3", + "value": "💪🏼" + }, + { + "key": "flexed_biceps_type_4", + "value": "💪🏽" + }, + { + "key": "flexed_biceps_type_5", + "value": "💪🏾" + }, + { + "key": "flexed_biceps_type_6", + "value": "💪🏿" + }, + { + "key": "folded_hands", + "value": "🙏" + }, + { + "key": "folded_hands_type_1_2", + "value": "🙏🏻" + }, + { + "key": "folded_hands_type_3", + "value": "🙏🏼" + }, + { + "key": "folded_hands_type_4", + "value": "🙏🏽" + }, + { + "key": "folded_hands_type_5", + "value": "🙏🏾" + }, + { + "key": "folded_hands_type_6", + "value": "🙏🏿" + }, + { + "key": "up_pointing_index", + "value": "☝" + }, + { + "key": "up_pointing_index_type_1_2", + "value": "☝🏻" + }, + { + "key": "up_pointing_index_type_3", + "value": "☝🏼" + }, + { + "key": "up_pointing_index_type_4", + "value": "☝🏽" + }, + { + "key": "up_pointing_index_type_5", + "value": "☝🏾" + }, + { + "key": "up_pointing_index_type_6", + "value": "☝🏿" + }, + { + "key": "up_pointing_backhand_index", + "value": "👆" + }, + { + "key": "up_pointing_backhand_index_type_1_2", + "value": "👆🏻" + }, + { + "key": "up_pointing_backhand_index_type_3", + "value": "👆🏼" + }, + { + "key": "up_pointing_backhand_index_type_4", + "value": "👆🏽" + }, + { + "key": "up_pointing_backhand_index_type_5", + "value": "👆🏾" + }, + { + "key": "up_pointing_backhand_index_type_6", + "value": "👆🏿" + }, + { + "key": "down_pointing_backhand_index", + "value": "👇" + }, + { + "key": "down_pointing_backhand_index_type_1_2", + "value": "👇🏻" + }, + { + "key": "down_pointing_backhand_index_type_3", + "value": "👇🏼" + }, + { + "key": "down_pointing_backhand_index_type_4", + "value": "👇🏽" + }, + { + "key": "down_pointing_backhand_index_type_5", + "value": "👇🏾" + }, + { + "key": "down_pointing_backhand_index_type_6", + "value": "👇🏿" + }, + { + "key": "left_pointing_backhand_index", + "value": "👈" + }, + { + "key": "left_pointing_backhand_index_type_1_2", + "value": "👈🏻" + }, + { + "key": "left_pointing_backhand_index_type_3", + "value": "👈🏼" + }, + { + "key": "left_pointing_backhand_index_type_4", + "value": "👈🏽" + }, + { + "key": "left_pointing_backhand_index_type_5", + "value": "👈🏾" + }, + { + "key": "left_pointing_backhand_index_type_6", + "value": "👈🏿" + }, + { + "key": "right_pointing_backhand_index", + "value": "👉" + }, + { + "key": "right_pointing_backhand_index_type_1_2", + "value": "👉🏻" + }, + { + "key": "right_pointing_backhand_index_type_3", + "value": "👉🏼" + }, + { + "key": "right_pointing_backhand_index_type_4", + "value": "👉🏽" + }, + { + "key": "right_pointing_backhand_index_type_5", + "value": "👉🏾" + }, + { + "key": "right_pointing_backhand_index_type_6", + "value": "👉🏿" + }, + { + "key": "reverse_middle_finger", + "value": "🖕" + }, + { + "key": "reverse_middle_finger_type_1_2", + "value": "🖕🏻" + }, + { + "key": "reverse_middle_finger_type_3", + "value": "🖕🏼" + }, + { + "key": "reverse_middle_finger_type_4", + "value": "🖕🏽" + }, + { + "key": "reverse_middle_finger_type_5", + "value": "🖕🏾" + }, + { + "key": "reverse_middle_finger_type_6", + "value": "🖕🏿" + }, + { + "key": "raised_hand_fingers_splayed", + "value": "🖐" + }, + { + "key": "raised_hand_fingers_splayed_type_1_2", + "value": "🖐🏻" + }, + { + "key": "raised_hand_fingers_splayed_type_3", + "value": "🖐🏼" + }, + { + "key": "raised_hand_fingers_splayed_type_4", + "value": "🖐🏽" + }, + { + "key": "raised_hand_fingers_splayed_type_5", + "value": "🖐🏾" + }, + { + "key": "raised_hand_fingers_splayed_type_6", + "value": "🖐🏿" + }, + { + "key": "sign_of_horn", + "value": "🤘" + }, + { + "key": "sign_of_horn_type_1_2", + "value": "🤘🏻" + }, + { + "key": "sign_of_horn_type_3", + "value": "🤘🏼" + }, + { + "key": "sign_of_horn_type_4", + "value": "🤘🏽" + }, + { + "key": "sign_of_horn_type_5", + "value": "🤘🏾" + }, + { + "key": "sign_of_horn_type_6", + "value": "🤘🏿" + }, + { + "key": "raised_hand_part_between_middle_ring", + "value": "🖖" + }, + { + "key": "raised_hand_part_between_middle_ring_type_1_2", + "value": "🖖🏻" + }, + { + "key": "raised_hand_part_between_middle_ring_type_3", + "value": "🖖🏼" + }, + { + "key": "raised_hand_part_between_middle_ring_type_4", + "value": "🖖🏽" + }, + { + "key": "raised_hand_part_between_middle_ring_type_5", + "value": "🖖🏾" + }, + { + "key": "raised_hand_part_between_middle_ring_type_6", + "value": "🖖🏿" + }, + { + "key": "writing_hand", + "value": "✍" + }, + { + "key": "writing_hand_type_1_2", + "value": "✍🏻" + }, + { + "key": "writing_hand_type_3", + "value": "✍🏼" + }, + { + "key": "writing_hand_type_4", + "value": "✍🏽" + }, + { + "key": "writing_hand_type_5", + "value": "✍🏾" + }, + { + "key": "writing_hand_type_6", + "value": "✍🏿" + }, + { + "key": "nail_polish", + "value": "💅" + }, + { + "key": "nail_polish_type_1_2", + "value": "💅🏻" + }, + { + "key": "nail_polish_type_3", + "value": "💅🏼" + }, + { + "key": "nail_polish_type_4", + "value": "💅🏽" + }, + { + "key": "nail_polish_type_5", + "value": "💅🏾" + }, + { + "key": "nail_polish_type_6", + "value": "💅🏿" + }, + { + "key": "mouth", + "value": "👄" + }, + { + "key": "tongue", + "value": "👅" + }, + { + "key": "ear", + "value": "👂" + }, + { + "key": "ear_type_1_2", + "value": "👂🏻" + }, + { + "key": "ear_type_3", + "value": "👂🏼" + }, + { + "key": "ear_type_4", + "value": "👂🏽" + }, + { + "key": "ear_type_5", + "value": "👂🏾" + }, + { + "key": "ear_type_6", + "value": "👂🏿" + }, + { + "key": "nose", + "value": "👃" + }, + { + "key": "nose_type_1_2", + "value": "👃🏻" + }, + { + "key": "nose_type_3", + "value": "👃🏼" + }, + { + "key": "nose_type_4", + "value": "👃🏽" + }, + { + "key": "nose_type_5", + "value": "👃🏾" + }, + { + "key": "nose_type_6", + "value": "👃🏿" + }, + { + "key": "eye", + "value": "👁" + }, + { + "key": "eyes", + "value": "👀" + }, + { + "key": "bust_in_silhouette", + "value": "👤" + }, + { + "key": "busts_in_silhouette", + "value": "👥" + }, + { + "key": "speaking_head_in_silhouette", + "value": "🗣" + }, + { + "key": "baby", + "value": "👶" + }, + { + "key": "baby_type_1_2", + "value": "👶🏻" + }, + { + "key": "baby_type_3", + "value": "👶🏼" + }, + { + "key": "baby_type_4", + "value": "👶🏽" + }, + { + "key": "baby_type_5", + "value": "👶🏾" + }, + { + "key": "baby_type_6", + "value": "👶🏿" + }, + { + "key": "boy", + "value": "👦" + }, + { + "key": "boy_type_1_2", + "value": "👦🏻" + }, + { + "key": "boy_type_3", + "value": "👦🏼" + }, + { + "key": "boy_type_4", + "value": "👦🏽" + }, + { + "key": "boy_type_5", + "value": "👦🏾" + }, + { + "key": "boy_type_6", + "value": "👦🏿" + }, + { + "key": "girl", + "value": "👧" + }, + { + "key": "girl_type_1_2", + "value": "👧🏻" + }, + { + "key": "girl_type_3", + "value": "👧🏼" + }, + { + "key": "girl_type_4", + "value": "👧🏽" + }, + { + "key": "girl_type_5", + "value": "👧🏾" + }, + { + "key": "girl_type_6", + "value": "👧🏿" + }, + { + "key": "man", + "value": "👨" + }, + { + "key": "man_type_1_2", + "value": "👨🏻" + }, + { + "key": "man_type_3", + "value": "👨🏼" + }, + { + "key": "man_type_4", + "value": "👨🏽" + }, + { + "key": "man_type_5", + "value": "👨🏾" + }, + { + "key": "man_type_6", + "value": "👨🏿" + }, + { + "key": "women", + "value": "👩" + }, + { + "key": "women_type_1_2", + "value": "👩🏻" + }, + { + "key": "women_type_3", + "value": "👩🏼" + }, + { + "key": "women_type_4", + "value": "👩🏽" + }, + { + "key": "women_type_5", + "value": "👩🏾" + }, + { + "key": "women_type_6", + "value": "👩🏿" + }, + { + "key": "person_with_blond_hair", + "value": "👱" + }, + { + "key": "person_with_blond_hair_type_1_2", + "value": "👱🏻" + }, + { + "key": "person_with_blond_hair_type_3", + "value": "👱🏼" + }, + { + "key": "person_with_blond_hair_type_4", + "value": "👱🏽" + }, + { + "key": "person_with_blond_hair_type_5", + "value": "👱🏾" + }, + { + "key": "person_with_blond_hair_type_6", + "value": "👱🏿" + }, + { + "key": "older_man", + "value": "👴" + }, + { + "key": "older_man_type_1_2", + "value": "👴🏻" + }, + { + "key": "older_man_type_3", + "value": "👴🏼" + }, + { + "key": "older_man_type_4", + "value": "👴🏽" + }, + { + "key": "older_man_type_5", + "value": "👴🏾" + }, + { + "key": "older_man_type_6", + "value": "👴🏿" + }, + { + "key": "older_women", + "value": "👵" + }, + { + "key": "older_women_type_1_2", + "value": "👵🏻" + }, + { + "key": "older_women_type_3", + "value": "👵🏼" + }, + { + "key": "older_women_type_4", + "value": "👵🏽" + }, + { + "key": "older_women_type_5", + "value": "👵🏾" + }, + { + "key": "older_women_type_6", + "value": "👵🏿" + }, + { + "key": "man_with_gua_pi_mao", + "value": "👲" + }, + { + "key": "man_with_gua_pi_mao_type_1_2", + "value": "👲🏼" + }, + { + "key": "man_with_gua_pi_mao_type_3", + "value": "👲🏼" + }, + { + "key": "man_with_gua_pi_mao_type_4", + "value": "👲🏽" + }, + { + "key": "man_with_gua_pi_mao_type_5", + "value": "👲🏾" + }, + { + "key": "man_with_gua_pi_mao_type_6", + "value": "👲🏿" + }, + { + "key": "man_with_turban", + "value": "👳" + }, + { + "key": "man_with_turban_type_1_2", + "value": "👳🏻" + }, + { + "key": "man_with_turban_type_3", + "value": "👳🏼" + }, + { + "key": "man_with_turban_type_4", + "value": "👳🏽" + }, + { + "key": "man_with_turban_type_5", + "value": "👳🏾" + }, + { + "key": "man_with_turban_type_6", + "value": "👳🏿" + }, + { + "key": "police_officer", + "value": "👮" + }, + { + "key": "police_officer_type_1_2", + "value": "👮🏻" + }, + { + "key": "police_officer_type_3", + "value": "👮🏼" + }, + { + "key": "police_officer_type_4", + "value": "👮🏽" + }, + { + "key": "police_officer_type_5", + "value": "👮🏾" + }, + { + "key": "police_officer_type_6", + "value": "👮🏿" + }, + { + "key": "construction_worker", + "value": "👷" + }, + { + "key": "construction_worker_type_1_2", + "value": "👷🏻" + }, + { + "key": "construction_worker_type_3", + "value": "👷🏼" + }, + { + "key": "construction_worker_type_4", + "value": "👷🏽" + }, + { + "key": "construction_worker_type_5", + "value": "👷🏾" + }, + { + "key": "construction_worker_type_6", + "value": "👷🏿" + }, + { + "key": "guards_man", + "value": "💂" + }, + { + "key": "guards_man_type_1_2", + "value": "💂🏻" + }, + { + "key": "guards_man_type_3", + "value": "💂🏼" + }, + { + "key": "guards_man_type_4", + "value": "💂🏽" + }, + { + "key": "guards_man_type_5", + "value": "💂🏾" + }, + { + "key": "guards_man_type_6", + "value": "💂🏿" + }, + { + "key": "spy", + "value": "🕵" + }, + { + "key": "father_christmas", + "value": "🎅" + }, + { + "key": "father_christmas_type_1_2", + "value": "🎅🏻" + }, + { + "key": "father_christmas_type_3", + "value": "🎅🏼" + }, + { + "key": "father_christmas_type_4", + "value": "🎅🏽" + }, + { + "key": "father_christmas_type_5", + "value": "🎅🏾" + }, + { + "key": "father_christmas_type_6", + "value": "🎅🏿" + }, + { + "key": "baby_angel", + "value": "👼" + }, + { + "key": "baby_angel_type_1_2", + "value": "👼🏻" + }, + { + "key": "baby_angel_type_3", + "value": "👼🏼" + }, + { + "key": "baby_angel_type_4", + "value": "👼🏽" + }, + { + "key": "baby_angel_type_5", + "value": "👼🏾" + }, + { + "key": "baby_angel_type_6", + "value": "👼🏿" + }, + { + "key": "princess", + "value": "👸" + }, + { + "key": "princess_type_1_2", + "value": "👸🏻" + }, + { + "key": "princess_type_3", + "value": "👸🏼" + }, + { + "key": "princess_type_4", + "value": "👸🏽" + }, + { + "key": "princess_type_5", + "value": "👸🏾" + }, + { + "key": "princess_type_6", + "value": "👸🏿" + }, + { + "key": "bride_with_veil", + "value": "👰" + }, + { + "key": "bride_with_veil_type_1_2", + "value": "👰🏻" + }, + { + "key": "bride_with_veil_type_3", + "value": "👰🏼" + }, + { + "key": "bride_with_veil_type_4", + "value": "👰🏽" + }, + { + "key": "bride_with_veil_type_5", + "value": "👰🏾" + }, + { + "key": "bride_with_veil_type_6", + "value": "👰🏿" + }, + { + "key": "pedestrian", + "value": "🚶" + }, + { + "key": "pedestrian_type_1_2", + "value": "🚶🏻" + }, + { + "key": "pedestrian_type_3", + "value": "🚶🏼" + }, + { + "key": "pedestrian_type_4", + "value": "🚶🏽" + }, + { + "key": "pedestrian_type_5", + "value": "🚶🏾" + }, + { + "key": "pedestrian_type_6", + "value": "🚶🏿" + }, + { + "key": "runner", + "value": "🏃" + }, + { + "key": "runner_type_1_2", + "value": "🏃🏻" + }, + { + "key": "runner_type_3", + "value": "🏃🏼" + }, + { + "key": "runner_type_4", + "value": "🏃🏽" + }, + { + "key": "runner_type_5", + "value": "🏃🏾" + }, + { + "key": "runner_type_6", + "value": "🏃🏿" + }, + { + "key": "dancer", + "value": "💃" + }, + { + "key": "dancer_type_1_2", + "value": "💃🏻" + }, + { + "key": "dancer_type_3", + "value": "💃🏼" + }, + { + "key": "dancer_type_4", + "value": "💃🏽" + }, + { + "key": "dancer_type_5", + "value": "💃🏾" + }, + { + "key": "dancer_type_6", + "value": "💃🏿" + }, + { + "key": "women_with_bunny_years", + "value": "👯" + }, + { + "key": "man_women_holding_hands", + "value": "👫" + }, + { + "key": "two_man_holding_hands", + "value": "👬" + }, + { + "key": "two_women_holding_hands", + "value": "👭" + }, + { + "key": "person_bowing_deeply", + "value": "🙇" + }, + { + "key": "person_bowing_deeply_type_1_2", + "value": "🙇🏻" + }, + { + "key": "person_bowing_deeply_type_3", + "value": "🙇🏼" + }, + { + "key": "person_bowing_deeply_type_4", + "value": "🙇🏽" + }, + { + "key": "person_bowing_deeply_type_5", + "value": "🙇🏾" + }, + { + "key": "person_bowing_deeply_type_6", + "value": "🙇🏿" + }, + { + "key": "information_desk_person", + "value": "💁" + }, + { + "key": "information_desk_person_type_1_2", + "value": "💁🏻" + }, + { + "key": "information_desk_person_type_3", + "value": "💁🏼" + }, + { + "key": "information_desk_person_type_4", + "value": "💁🏽" + }, + { + "key": "information_desk_person_type_5", + "value": "💁🏾" + }, + { + "key": "information_desk_person_type_6", + "value": "💁🏿" + }, + { + "key": "face_with_no_good_gesture", + "value": "🙅" + }, + { + "key": "face_with_no_good_gesture_type_1_2", + "value": "🙅🏻" + }, + { + "key": "face_with_no_good_gesture_type_3", + "value": "🙅🏼" + }, + { + "key": "face_with_no_good_gesture_type_4", + "value": "🙅🏽" + }, + { + "key": "face_with_no_good_gesture_type_5", + "value": "🙅🏾" + }, + { + "key": "face_with_no_good_gesture_type_6", + "value": "🙅🏿" + }, + { + "key": "face_with_ok_gesture", + "value": "🙆" + }, + { + "key": "face_with_ok_gesture_type_1_2", + "value": "🙆🏻" + }, + { + "key": "face_with_ok_gesture_type_3", + "value": "🙆🏼" + }, + { + "key": "face_with_ok_gesture_type_4", + "value": "🙆🏽" + }, + { + "key": "face_with_ok_gesture_type_5", + "value": "🙆🏾" + }, + { + "key": "face_with_ok_gesture_type_6", + "value": "🙆🏿" + }, + { + "key": "happy_person_raise_one_hand", + "value": "🙋" + }, + { + "key": "happy_person_raise_one_hand_type_1_2", + "value": "🙋🏻" + }, + { + "key": "happy_person_raise_one_hand_type_3", + "value": "🙋🏼" + }, + { + "key": "happy_person_raise_one_hand_type_4", + "value": "🙋🏽" + }, + { + "key": "happy_person_raise_one_hand_type_5", + "value": "🙋🏾" + }, + { + "key": "happy_person_raise_one_hand_type_6", + "value": "🙋🏿" + }, + { + "key": "person_with_pouting_face", + "value": "🙎" + }, + { + "key": "person_with_pouting_face_type_1_2", + "value": "🙎🏻" + }, + { + "key": "person_with_pouting_face_type_3", + "value": "🙎🏼" + }, + { + "key": "person_with_pouting_face_type_4", + "value": "🙎🏽" + }, + { + "key": "person_with_pouting_face_type_5", + "value": "🙎🏾" + }, + { + "key": "person_with_pouting_face_type_6", + "value": "🙎🏿" + }, + { + "key": "person_frowning", + "value": "🙍" + }, + { + "key": "person_frowning_type_1_2", + "value": "🙍🏻" + }, + { + "key": "person_frowning_type_3", + "value": "🙍🏼" + }, + { + "key": "person_frowning_type_4", + "value": "🙍🏽" + }, + { + "key": "person_frowning_type_5", + "value": "🙍🏾" + }, + { + "key": "person_frowning_type_6", + "value": "🙍🏿" + }, + { + "key": "haircut", + "value": "💇" + }, + { + "key": "haircut_type_1_2", + "value": "💇🏻" + }, + { + "key": "haircut_type_3", + "value": "💇🏼" + }, + { + "key": "haircut_type_4", + "value": "💇🏽" + }, + { + "key": "haircut_type_5", + "value": "💇🏾" + }, + { + "key": "haircut_type_6", + "value": "💇🏿" + }, + { + "key": "face_massage", + "value": "💆" + }, + { + "key": "face_massage_type_1_2", + "value": "💆🏻" + }, + { + "key": "face_massage_type_3", + "value": "💆🏻" + }, + { + "key": "face_massage_type_4", + "value": "💆🏽" + }, + { + "key": "face_massage_type_5", + "value": "💆🏾" + }, + { + "key": "face_massage_type_6", + "value": "💆🏿" + }, + { + "key": "couple_with_heart", + "value": "💑" + }, + { + "key": "couple_with_heart_woman", + "value": "👩❤️👩" + }, + { + "key": "couple_with_heart_man", + "value": "👨❤️👨" + }, + { + "key": "kiss", + "value": "💏" + }, + { + "key": "kiss_woman", + "value": "👩❤️💋👩" + }, + { + "key": "kiss_man", + "value": "👨❤️💋👨" + }, + { + "key": "family", + "value": "👪" + }, + { + "key": "family_man_women_girl", + "value": "👨👩👧" + }, + { + "key": "family_man_women_girl_boy", + "value": "👨👩👧👦" + }, + { + "key": "family_man_women_boy_boy", + "value": "👨👩👦👦" + }, + { + "key": "family_man_women_girl_girl", + "value": "👨👩👧👧" + }, + { + "key": "family_woman_women_boy", + "value": "👩👩👦" + }, + { + "key": "family_woman_women_girl", + "value": "👩👩👧" + }, + { + "key": "family_woman_women_girl_boy", + "value": "👩👩👧👦" + }, + { + "key": "family_woman_women_boy_boy", + "value": "👩👩👦👦" + }, + { + "key": "family_woman_women_girl_girl", + "value": "👩👩👧👧" + }, + { + "key": "family_man_man_boy", + "value": "👨👨👦" + }, + { + "key": "family_man_man_girl", + "value": "👨👨👧" + }, + { + "key": "family_man_man_girl_boy", + "value": "👨👨👧👦" + }, + { + "key": "family_man_man_boy_boy", + "value": "👨👨👦👦" + }, + { + "key": "family_man_man_girl_girl", + "value": "👨👨👧👧" + }, + { + "key": "woman_clothes", + "value": "👚" + }, + { + "key": "t_shirt", + "value": "👕" + }, + { + "key": "jeans", + "value": "👖" + }, + { + "key": "necktie", + "value": "👔" + }, + { + "key": "dress", + "value": "👗" + }, + { + "key": "bikini", + "value": "👙" + }, + { + "key": "kimono", + "value": "👘" + }, + { + "key": "lipstick", + "value": "💄" + }, + { + "key": "kiss_mark", + "value": "💋" + }, + { + "key": "footprints", + "value": "👣" + }, + { + "key": "high_heeled_shoe", + "value": "👠" + }, + { + "key": "woman_sandal", + "value": "👡" + }, + { + "key": "woman_boots", + "value": "👢" + }, + { + "key": "man_shoe", + "value": "👞" + }, + { + "key": "athletic_shoe", + "value": "👟" + }, + { + "key": "woman_hat", + "value": "👒" + }, + { + "key": "top_hat", + "value": "🎩" + }, + { + "key": "graduation_cap", + "value": "🎓" + }, + { + "key": "crown", + "value": "👑" + }, + { + "key": "helmet_with_white_cross", + "value": "⛑" + }, + { + "key": "school_satchel", + "value": "🎒" + }, + { + "key": "pouch", + "value": "👝" + }, + { + "key": "purse", + "value": "👛" + }, + { + "key": "handbag", + "value": "👜" + }, + { + "key": "briefcase", + "value": "💼" + }, + { + "key": "eye_glasses", + "value": "👓" + }, + { + "key": "dark_sun_glasses", + "value": "🕶" + }, + { + "key": "ring", + "value": "💍" + }, + { + "key": "closed_umbrella", + "value": "🌂" + } + ] + } +}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/Symbols.json b/nikola/plugins/shortcode/emoji/data/Symbols.json new file mode 100644 index 0000000..2dd5454 --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Symbols.json @@ -0,0 +1,1082 @@ +{ + "symbols": { + "symbol": [ + { + "key": "heavy_black_heart", + "value": "❤" + }, + { + "key": "yellow_heart", + "value": "💛" + }, + { + "key": "green_heart", + "value": "💚" + }, + { + "key": "blue_heart", + "value": "💙" + }, + { + "key": "purple_heart", + "value": "💜" + }, + { + "key": "broken_heart", + "value": "💔" + }, + { + "key": "heavy_heart_exclamation_mark_ornament", + "value": "❣" + }, + { + "key": "two_hearts", + "value": "💕" + }, + { + "key": "revolving_hearts", + "value": "💞" + }, + { + "key": "beating_heart", + "value": "💓" + }, + { + "key": "growing_heart", + "value": "💗" + }, + { + "key": "sparkling_heart", + "value": "💖" + }, + { + "key": "heart_with_arrow", + "value": "💘" + }, + { + "key": "heart_with_ribbon", + "value": "💝" + }, + { + "key": "heart_decoration", + "value": "💟" + }, + { + "key": "peace_symbol", + "value": "☮" + }, + { + "key": "latin_cross", + "value": "✝" + }, + { + "key": "star_and_crescent", + "value": "☪" + }, + { + "key": "om_symbol", + "value": "🕉" + }, + { + "key": "wheel_of_dharma", + "value": "☸" + }, + { + "key": "star_of_david", + "value": "✡" + }, + { + "key": "six_pointed_star_with_middle_dot", + "value": "🔯" + }, + { + "key": "menorah_with_nine_branches", + "value": "🕎" + }, + { + "key": "yin_yang", + "value": "☯" + }, + { + "key": "orthodox_cross", + "value": "☦" + }, + { + "key": "place_of_worship", + "value": "🛐" + }, + { + "key": "ophiuchus", + "value": "⛎" + }, + { + "key": "aries", + "value": "♈" + }, + { + "key": "taurus", + "value": "♉" + }, + { + "key": "gemini", + "value": "♊" + }, + { + "key": "cancer", + "value": "♋" + }, + { + "key": "leo", + "value": "♌" + }, + { + "key": "virgo", + "value": "♍" + }, + { + "key": "libra", + "value": "♎" + }, + { + "key": "scorpius", + "value": "♏" + }, + { + "key": "sagittarius", + "value": "♐" + }, + { + "key": "capricorn", + "value": "♑" + }, + { + "key": "aquarius", + "value": "♒" + }, + { + "key": "pisces", + "value": "♓" + }, + { + "key": "squared_id", + "value": "🆔" + }, + { + "key": "atom_symbol", + "value": "⚛" + }, + { + "key": "squared_cjk_unified_ideograph_7a7a", + "value": "🈳" + }, + { + "key": "squared_cjk_unified_ideograph_5272", + "value": "🈹" + }, + { + "key": "radioactive_sign", + "value": "☢" + }, + { + "key": "biohazard_sign", + "value": "☣" + }, + { + "key": "mobile_phone_off", + "value": "📴" + }, + { + "key": "vibration_mode", + "value": "📳" + }, + { + "key": "squared_cjk_unified_ideograph_6709", + "value": "🈶" + }, + { + "key": "squared_cjk_unified_ideograph_7121", + "value": "🈚" + }, + { + "key": "squared_cjk_unified_ideograph_7533", + "value": "🈸" + }, + { + "key": "squared_cjk_unified_ideograph_55b6", + "value": "🈺" + }, + { + "key": "squared_cjk_unified_ideograph_6708", + "value": "🈷" + }, + { + "key": "eight_pointed_black_star", + "value": "✴" + }, + { + "key": "squared_vs", + "value": "🆚" + }, + { + "key": "circled_ideograph_accept", + "value": "🉑" + }, + { + "key": "white_flower", + "value": "💮" + }, + { + "key": "circled_ideograph_advantage", + "value": "🉐" + }, + { + "key": "circled_ideograph_secret", + "value": "㊙" + }, + { + "key": "circled_ideograph_congratulation", + "value": "㊗" + }, + { + "key": "squared_cjk_unified_ideograph_5408", + "value": "🈴" + }, + { + "key": "squared_cjk_unified_ideograph_6e80", + "value": "🈵" + }, + { + "key": "squared_cjk_unified_ideograph_7981", + "value": "🈲" + }, + { + "key": "negative_squared_latin_capital_letter_a", + "value": "🅰" + }, + { + "key": "negative_squared_latin_capital_letter_b", + "value": "🅱" + }, + { + "key": "negative_squared_ab", + "value": "🆎" + }, + { + "key": "squared_cl", + "value": "🆑" + }, + { + "key": "negative_squared_latin_capital_letter_o", + "value": "🅾" + }, + { + "key": "squared_sos", + "value": "🆘" + }, + { + "key": "no_entry", + "value": "⛔" + }, + { + "key": "name_badge", + "value": "📛" + }, + { + "key": "no_entry_sign", + "value": "🚫" + }, + { + "key": "cross_mark", + "value": "❌" + }, + { + "key": "heavy_large_circle", + "value": "⭕" + }, + { + "key": "anger_symbol", + "value": "💢" + }, + { + "key": "hot_springs", + "value": "♨" + }, + { + "key": "no_pedestrians", + "value": "🚷" + }, + { + "key": "do_not_litter_symbol", + "value": "🚯" + }, + { + "key": "no_bi_cycles", + "value": "🚳" + }, + { + "key": "non_potable_water_symbol", + "value": "🚱" + }, + { + "key": "no_one_under_eighteen_symbol", + "value": "🔞" + }, + { + "key": "no_mobile_phones", + "value": "📵" + }, + { + "key": "heavy_exclamation_mark_symbol", + "value": "❗" + }, + { + "key": "white_exclamation_mark_ornament", + "value": "❕" + }, + { + "key": "black_question_mark_ornament", + "value": "❓" + }, + { + "key": "white_question_mark_ornament", + "value": "❔" + }, + { + "key": "double_exclamation_mark", + "value": "‼" + }, + { + "key": "exclamation_question_mark", + "value": "⁉" + }, + { + "key": "hundred_points_symbol", + "value": "💯" + }, + { + "key": "low_brightness_symbol", + "value": "🔅" + }, + { + "key": "high_brightness_symbol", + "value": "🔆" + }, + { + "key": "trident_emblem", + "value": "🔱" + }, + { + "key": "fleur_de_lis", + "value": "⚜" + }, + { + "key": "part_alternation_mark", + "value": "〽" + }, + { + "key": "warning_sign", + "value": "⚠" + }, + { + "key": "children_crossing", + "value": "🚸" + }, + { + "key": "japanese_symbol_for_beginner", + "value": "🔰" + }, + { + "key": "black_universal_recycling_symbol", + "value": "♻" + }, + { + "key": "squared_cjk_unified_ideograph_6307", + "value": "🈯" + }, + { + "key": "chart_with_upwards_trend_and_yen_sign", + "value": "💹" + }, + { + "key": "sparkle", + "value": "❇" + }, + { + "key": "eight_spoked_asterisk", + "value": "✳" + }, + { + "key": "negative_squared_crossmark", + "value": "❎" + }, + { + "key": "white_heavy_checkmark", + "value": "✅" + }, + { + "key": "diamond_shape_with_a_dot_inside", + "value": "💠" + }, + { + "key": "cyclone", + "value": "🌀" + }, + { + "key": "double_curly_loop", + "value": "➿" + }, + { + "key": "globe_with_meridians", + "value": "🌐" + }, + { + "key": "circled_latin_capital_letter_m", + "value": "ⓜ" + }, + { + "key": "automated_teller_machine", + "value": "🏧" + }, + { + "key": "squared_katakanasa", + "value": "🈂" + }, + { + "key": "passport_control", + "value": "🛂" + }, + { + "key": "customs", + "value": "🛃" + }, + { + "key": "baggage_claim", + "value": "🛄" + }, + { + "key": "left_luggage", + "value": "🛅" + }, + { + "key": "wheel_chair_symbol", + "value": "♿" + }, + { + "key": "no_smoking_symbol", + "value": "🚭" + }, + { + "key": "water_closet", + "value": "🚾" + }, + { + "key": "negative_squared_letter_p", + "value": "🅿" + }, + { + "key": "potable_water_symbol", + "value": "🚰" + }, + { + "key": "mens_symbol", + "value": "🚹" + }, + { + "key": "womens_symbol", + "value": "🚺" + }, + { + "key": "baby_symbol", + "value": "🚼" + }, + { + "key": "restroom", + "value": "🚻" + }, + { + "key": "put_litter_in_its_place", + "value": "🚮" + }, + { + "key": "cinema", + "value": "🎦" + }, + { + "key": "antenna_with_bars", + "value": "📶" + }, + { + "key": "squared_katakana_koko", + "value": "🈁" + }, + { + "key": "squared_ng", + "value": "🆖" + }, + { + "key": "squared_ok", + "value": "🆗" + }, + { + "key": "squared_exclamation_mark", + "value": "🆙" + }, + { + "key": "squared_cool", + "value": "🆒" + }, + { + "key": "squared_new", + "value": "🆕" + }, + { + "key": "squared_free", + "value": "🆓" + }, + { + "key": "keycap_digit_zero", + "value": "0⃣" + }, + { + "key": "keycap_digit_one", + "value": "1⃣" + }, + { + "key": "keycap_digit_two", + "value": "2⃣" + }, + { + "key": "keycap_digit_three", + "value": "3⃣" + }, + { + "key": "keycap_digit_four", + "value": "4⃣" + }, + { + "key": "keycap_digit_five", + "value": "5⃣" + }, + { + "key": "keycap_digit_six", + "value": "6⃣" + }, + { + "key": "keycap_digit_seven", + "value": "7⃣" + }, + { + "key": "keycap_digit_eight", + "value": "8⃣" + }, + { + "key": "keycap_digit_nine", + "value": "9⃣" + }, + { + "key": "keycap_ten", + "value": "🔟" + }, + { + "key": "input_symbol_for_numbers", + "value": "🔢" + }, + { + "key": "black_right_pointing_triangle", + "value": "▶" + }, + { + "key": "double_vertical_bar", + "value": "⏸" + }, + { + "key": "blk_rgt_point_triangle_dbl_vertical_bar", + "value": "⏯" + }, + { + "key": "black_square_for_stop", + "value": "⏹" + }, + { + "key": "black_circle_for_record", + "value": "⏺" + }, + { + "key": "blk_rgt_point_dbl_triangle_vertical_bar", + "value": "⏭" + }, + { + "key": "blk_lft_point_dbl_triangle_vertical_bar", + "value": "⏮" + }, + { + "key": "blk_rgt_point_dbl_triangle", + "value": "⏩" + }, + { + "key": "blk_lft_point_dbl_triangle", + "value": "⏪" + }, + { + "key": "twisted_rightwards_arrows", + "value": "🔀" + }, + { + "key": "cwise_rgt_lft_open_circle_arrow", + "value": "🔁" + }, + { + "key": "cwise_rgt_lft_open_circle_arrow_overlay", + "value": "🔂" + }, + { + "key": "blk_lft_point_triangle", + "value": "◀" + }, + { + "key": "up_point_small_red_triangle", + "value": "🔼" + }, + { + "key": "down_point_small_red_triangle", + "value": "🔽" + }, + { + "key": "blk_up_point_double_triangle", + "value": "⏫" + }, + { + "key": "blk_down_point_double_triangle", + "value": "⏬" + }, + { + "key": "black_rightwards_arrow", + "value": "➡" + }, + { + "key": "leftwards_black_arrow", + "value": "⬅" + }, + { + "key": "upwards_black_arrow", + "value": "⬆" + }, + { + "key": "downwards_black_arrow", + "value": "⬇" + }, + { + "key": "northeast_arrow", + "value": "↗" + }, + { + "key": "southeast_arrow", + "value": "↘" + }, + { + "key": "south_west_arrow", + "value": "↙" + }, + { + "key": "north_west_arrow", + "value": "↖" + }, + { + "key": "up_down_arrow", + "value": "↕" + }, + { + "key": "left_right_arrow", + "value": "↔" + }, + { + "key": "acwise_down_up_open_circle_arrow", + "value": "🔄" + }, + { + "key": "rightwards_arrow_with_hook", + "value": "↪" + }, + { + "key": "leftwards_arrow_with_hook", + "value": "↩" + }, + { + "key": "arrow_point_rgt_then_curving_up", + "value": "⤴" + }, + { + "key": "arrow_point_rgt_then_curving_down", + "value": "⤵" + }, + { + "key": "keycap_number_sign", + "value": "#⃣" + }, + { + "key": "keycap_asterisk", + "value": "*⃣" + }, + { + "key": "information_source", + "value": "ℹ" + }, + { + "key": "input_symbol_for_latin_letters", + "value": "🔤" + }, + { + "key": "input_symbol_latin_small_letters", + "value": "🔡" + }, + { + "key": "input_symbol_latin_capital_letters", + "value": "🔠" + }, + { + "key": "input_symbol_symbols", + "value": "🔣" + }, + { + "key": "musical_note", + "value": "🎵" + }, + { + "key": "multiple_musical_notes", + "value": "🎶" + }, + { + "key": "wavy_dash", + "value": "〰" + }, + { + "key": "curly_loop", + "value": "➰" + }, + { + "key": "heavy_check_mark", + "value": "✔" + }, + { + "key": "cwise_down_up_open_circle_arrows", + "value": "🔃" + }, + { + "key": "heavy_plus_sign", + "value": "➕" + }, + { + "key": "heavy_minus_sign", + "value": "➖" + }, + { + "key": "heavy_division_sign", + "value": "➗" + }, + { + "key": "heavy_multiplication_x", + "value": "✖" + }, + { + "key": "heavy_dollar_sign", + "value": "💲" + }, + { + "key": "currency_exchange", + "value": "💱" + }, + { + "key": "copyright_sign", + "value": "©" + }, + { + "key": "registered_sign", + "value": "®" + }, + { + "key": "trademark_sign", + "value": "™" + }, + { + "key": "end_with_lft_arrow_above", + "value": "🔚" + }, + { + "key": "back_with_lft_arrow_above", + "value": "🔙" + }, + { + "key": "on_exclamation_lft_rgt_arrow", + "value": "🔛" + }, + { + "key": "top_with_up_arrow_above", + "value": "🔝" + }, + { + "key": "soon_right_arrow_above", + "value": "🔜" + }, + { + "key": "ballot_box_with_check", + "value": "☑" + }, + { + "key": "radio_button", + "value": "🔘" + }, + { + "key": "medium_white_circle", + "value": "⚪" + }, + { + "key": "medium_black_circle", + "value": "⚫" + }, + { + "key": "large_red_circle", + "value": "🔴" + }, + { + "key": "large_blue_circle", + "value": "🔵" + }, + { + "key": "small_orange_diamond", + "value": "🔸" + }, + { + "key": "small_blue_diamond", + "value": "🔹" + }, + { + "key": "large_orange_diamond", + "value": "🔶" + }, + { + "key": "large_blue_diamond", + "value": "🔷" + }, + { + "key": "up_point_red_triangle", + "value": "🔺" + }, + { + "key": "black_small_square", + "value": "▪" + }, + { + "key": "white_small_square", + "value": "▫" + }, + { + "key": "black_large_square", + "value": "⬛" + }, + { + "key": "white_large_square", + "value": "⬜" + }, + { + "key": "down_point_red_triangle", + "value": "🔻" + }, + { + "key": "black_medium_square", + "value": "◼" + }, + { + "key": "white_medium_square", + "value": "◻" + }, + { + "key": "black_medium_small_square", + "value": "◾" + }, + { + "key": "white_medium_small_square", + "value": "◽" + }, + { + "key": "black_square_button", + "value": "🔲" + }, + { + "key": "white_square_button", + "value": "🔳" + }, + { + "key": "speaker", + "value": "🔈" + }, + { + "key": "speaker_one_sound_wave", + "value": "🔉" + }, + { + "key": "speaker_three_sound_waves", + "value": "🔊" + }, + { + "key": "speaker_cancellation_stroke", + "value": "🔇" + }, + { + "key": "cheering_megaphone", + "value": "📣" + }, + { + "key": "public_address_loudspeaker", + "value": "📢" + }, + { + "key": "bell", + "value": "🔔" + }, + { + "key": "bell_with_cancellation_stroke", + "value": "🔕" + }, + { + "key": "playing_card_black_joker", + "value": "🃏" + }, + { + "key": "mahjong_tile_red_dragon", + "value": "🀄" + }, + { + "key": "black_spade_suit", + "value": "♠" + }, + { + "key": "black_club_suit", + "value": "♣" + }, + { + "key": "black_heart_suit", + "value": "♥" + }, + { + "key": "black_diamond_suit", + "value": "♦" + }, + { + "key": "flower_playing_cards", + "value": "🎴" + }, + { + "key": "eye_in_speech_bubble", + "value": "👁🗨" + }, + { + "key": "thought_balloon", + "value": "💭" + }, + { + "key": "right_anger_bubble", + "value": "🗯" + }, + { + "key": "speech_balloon", + "value": "💬" + }, + { + "key": "clock_face_one_o_clock", + "value": "🕐" + }, + { + "key": "clock_face_two_o_clock", + "value": "🕑" + }, + { + "key": "clock_face_three_o_clock", + "value": "🕒" + }, + { + "key": "clock_face_four_o_clock", + "value": "🕓" + }, + { + "key": "clock_face_five_o_clock", + "value": "🕔" + }, + { + "key": "clock_face_six_o_clock", + "value": "🕕" + }, + { + "key": "clock_face_seven_o_clock", + "value": "🕖" + }, + { + "key": "clock_face_eight_o_clock", + "value": "🕗" + }, + { + "key": "clock_face_nine_o_clock", + "value": "🕘" + }, + { + "key": "clock_face_ten_o_clock", + "value": "🕙" + }, + { + "key": "clock_face_eleven_o_clock", + "value": "🕚" + }, + { + "key": "clock_face_twelve_o_clock", + "value": "🕛" + }, + { + "key": "clock_face_one_thirty", + "value": "🕜" + }, + { + "key": "clock_face_two_thirty", + "value": "🕝" + }, + { + "key": "clock_face_three_thirty", + "value": "🕞" + }, + { + "key": "clock_face_four_thirty", + "value": "🕟" + }, + { + "key": "clock_face_five_thirty", + "value": "🕠" + }, + { + "key": "clock_face_six_thirty", + "value": "🕡" + }, + { + "key": "clock_face_seven_thirty", + "value": "🕢" + }, + { + "key": "clock_face_eight_thirty", + "value": "🕣" + }, + { + "key": "clock_face_nine_thirty", + "value": "🕤" + }, + { + "key": "clock_face_ten_thirty", + "value": "🕥" + }, + { + "key": "clock_face_eleven_thirty", + "value": "🕦" + }, + { + "key": "clock_face_twelve_thirty", + "value": "🕧" + } + ] + } +}
\ No newline at end of file diff --git a/nikola/plugins/shortcode/emoji/data/Travel.json b/nikola/plugins/shortcode/emoji/data/Travel.json new file mode 100644 index 0000000..e38b84f --- /dev/null +++ b/nikola/plugins/shortcode/emoji/data/Travel.json @@ -0,0 +1,466 @@ +{ + "travels": { + "travel": [ + { + "key": "automobile", + "value": "🚗" + }, + { + "key": "taxi", + "value": "🚕" + }, + { + "key": "recreational_vehicle", + "value": "🚙" + }, + { + "key": "bus", + "value": "🚌" + }, + { + "key": "trolley_bus", + "value": "🚎" + }, + { + "key": "racing_car", + "value": "🏎" + }, + { + "key": "police_car", + "value": "🚓" + }, + { + "key": "ambulance", + "value": "🚑" + }, + { + "key": "fire_engine", + "value": "🚒" + }, + { + "key": "minibus", + "value": "🚐" + }, + { + "key": "delivery_truck", + "value": "🚚" + }, + { + "key": "articulated_lorry", + "value": "🚛" + }, + { + "key": "tractor", + "value": "🚜" + }, + { + "key": "racing_motorcycle", + "value": "🏍" + }, + { + "key": "bicycle", + "value": "🚲" + }, + { + "key": "police_light", + "value": "🚨" + }, + { + "key": "on_coming_police_car", + "value": "🚔" + }, + { + "key": "on_coming_bus", + "value": "🚍" + }, + { + "key": "on_coming_automobile", + "value": "🚘" + }, + { + "key": "on_coming_taxi", + "value": "🚖" + }, + { + "key": "aerial_tramway", + "value": "🚡" + }, + { + "key": "mountain_cableway", + "value": "🚠" + }, + { + "key": "suspension_railway", + "value": "🚟" + }, + { + "key": "railway_car", + "value": "🚃" + }, + { + "key": "tramcar", + "value": "🚋" + }, + { + "key": "monorail", + "value": "🚝" + }, + { + "key": "high_speed_train", + "value": "🚄" + }, + { + "key": "high_speed_train_bullet_nose", + "value": "🚅" + }, + { + "key": "light_rail", + "value": "🚈" + }, + { + "key": "mountain_railway", + "value": "🚞" + }, + { + "key": "steam_locomotive", + "value": "🚂" + }, + { + "key": "train", + "value": "🚆" + }, + { + "key": "metro", + "value": "🚇" + }, + { + "key": "tram", + "value": "🚊" + }, + { + "key": "station", + "value": "🚉" + }, + { + "key": "helicopter", + "value": "🚁" + }, + { + "key": "small_airplane", + "value": "🛩" + }, + { + "key": "airplane", + "value": "✈" + }, + { + "key": "airplane_departure", + "value": "🛫" + }, + { + "key": "airplane_arriving", + "value": "🛬" + }, + { + "key": "sailboat", + "value": "⛵" + }, + { + "key": "motorboat", + "value": "🛥" + }, + { + "key": "speedboat", + "value": "🚤" + }, + { + "key": "ferry", + "value": "⛴" + }, + { + "key": "passenger_ship", + "value": "🛳" + }, + { + "key": "rocket", + "value": "🚀" + }, + { + "key": "satellite", + "value": "🛰" + }, + { + "key": "seat", + "value": "💺" + }, + { + "key": "anchor", + "value": "⚓" + }, + { + "key": "construction_sign", + "value": "🚧" + }, + { + "key": "fuel_pump", + "value": "⛽" + }, + { + "key": "bus_stop", + "value": "🚏" + }, + { + "key": "vertical_traffic_light", + "value": "🚦" + }, + { + "key": "horizontal_traffic_light", + "value": "🚥" + }, + { + "key": "chequered_flag", + "value": "🏁" + }, + { + "key": "ship", + "value": "🚢" + }, + { + "key": "ferris_wheel", + "value": "🎡" + }, + { + "key": "roller_coaster", + "value": "🎢" + }, + { + "key": "carousel_horse", + "value": "🎠" + }, + { + "key": "building_construction", + "value": "🏗" + }, + { + "key": "foggy", + "value": "🌁" + }, + { + "key": "tokyo_tower", + "value": "🗼" + }, + { + "key": "factory", + "value": "🏭" + }, + { + "key": "fountain", + "value": "⛲" + }, + { + "key": "moon_viewing_ceremony", + "value": "🎑" + }, + { + "key": "mountain", + "value": "⛰" + }, + { + "key": "snow_capped_mountain", + "value": "🏔" + }, + { + "key": "mount_fuji", + "value": "🗻" + }, + { + "key": "volcano", + "value": "🌋" + }, + { + "key": "silhouette_of_japan", + "value": "🗾" + }, + { + "key": "camping", + "value": "🏕" + }, + { + "key": "tent", + "value": "⛺" + }, + { + "key": "national_park", + "value": "🏞" + }, + { + "key": "motorway", + "value": "🛣" + }, + { + "key": "railway_track", + "value": "🛤" + }, + { + "key": "sunrise", + "value": "🌅" + }, + { + "key": "sunrise_over_mountain", + "value": "🌄" + }, + { + "key": "desert", + "value": "🏜" + }, + { + "key": "beach_with_umbrella", + "value": "🏖" + }, + { + "key": "desert_island", + "value": "🏝" + }, + { + "key": "sunset_over_buildings", + "value": "🌇" + }, + { + "key": "city_scape_at_dusk", + "value": "🌆" + }, + { + "key": "city_scape", + "value": "🏙" + }, + { + "key": "night_with_stars", + "value": "🌃" + }, + { + "key": "bridge_at_night", + "value": "🌉" + }, + { + "key": "milky_way", + "value": "🌌" + }, + { + "key": "shooting_star", + "value": "🌠" + }, + { + "key": "fire_work_sparkler", + "value": "🎇" + }, + { + "key": "fireworks", + "value": "🎆" + }, + { + "key": "rainbow", + "value": "🌈" + }, + { + "key": "house_buildings", + "value": "🏘" + }, + { + "key": "european_castle", + "value": "🏰" + }, + { + "key": "japanese_castle", + "value": "🏯" + }, + { + "key": "stadium", + "value": "🏟" + }, + { + "key": "statue_of_liberty", + "value": "🗽" + }, + { + "key": "house_building", + "value": "🏠" + }, + { + "key": "house_with_garden", + "value": "🏡" + }, + { + "key": "derelict_house_building", + "value": "🏚" + }, + { + "key": "office_building", + "value": "🏢" + }, + { + "key": "department_store", + "value": "🏬" + }, + { + "key": "japanese_post_office", + "value": "🏣" + }, + { + "key": "european_post_office", + "value": "🏤" + }, + { + "key": "hospital", + "value": "🏥" + }, + { + "key": "bank", + "value": "🏦" + }, + { + "key": "hotel", + "value": "🏨" + }, + { + "key": "convenience_store", + "value": "🏪" + }, + { + "key": "school", + "value": "🏫" + }, + { + "key": "love_hotel", + "value": "🏩" + }, + { + "key": "wedding", + "value": "💒" + }, + { + "key": "classical_building", + "value": "🏛" + }, + { + "key": "church", + "value": "⛪" + }, + { + "key": "mosque", + "value": "🕌" + }, + { + "key": "synagogue", + "value": "🕍" + }, + { + "key": "kaaba", + "value": "🕋" + }, + { + "key": "shinto_shrine", + "value": "⛩" + } + ] + } +} diff --git a/nikola/plugins/shortcode/gist.plugin b/nikola/plugins/shortcode/gist.plugin index cd19a72..b610763 100644 --- a/nikola/plugins/shortcode/gist.plugin +++ b/nikola/plugins/shortcode/gist.plugin @@ -3,7 +3,7 @@ name = gist module = gist [Nikola] -plugincategory = Shortcode +PluginCategory = Shortcode [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/shortcode/gist.py b/nikola/plugins/shortcode/gist.py index 64fd0d9..eb9e976 100644 --- a/nikola/plugins/shortcode/gist.py +++ b/nikola/plugins/shortcode/gist.py @@ -13,12 +13,6 @@ class Plugin(ShortcodePlugin): name = "gist" - def set_site(self, site): - """Set Nikola site.""" - self.site = site - site.register_shortcode('gist', self.handler) - return super(Plugin, self).set_site(site) - 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)) diff --git a/nikola/plugins/shortcode/listing.plugin b/nikola/plugins/shortcode/listing.plugin new file mode 100644 index 0000000..90fb6eb --- /dev/null +++ b/nikola/plugins/shortcode/listing.plugin @@ -0,0 +1,13 @@ +[Core] +name = listing_shortcode +module = listing + +[Nikola] +PluginCategory = Shortcode + +[Documentation] +author = Roberto Alsina +version = 0.1 +website = https://getnikola.com/ +description = Listing shortcode + diff --git a/nikola/plugins/shortcode/listing.py b/nikola/plugins/shortcode/listing.py new file mode 100644 index 0000000..b51365a --- /dev/null +++ b/nikola/plugins/shortcode/listing.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2017-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Listing shortcode (equivalent to reST’s listing directive).""" + +import os +from urllib.parse import urlunsplit + +import pygments + +from nikola.plugin_categories import ShortcodePlugin + + +class Plugin(ShortcodePlugin): + """Plugin for listing shortcode.""" + + name = "listing" + + def set_site(self, site): + """Set Nikola site.""" + self.site = site + Plugin.folders = site.config['LISTINGS_FOLDERS'] + return super().set_site(site) + + def handler(self, fname, language='text', linenumbers=False, filename=None, site=None, data=None, lang=None, post=None): + """Create HTML for a listing.""" + fname = fname.replace('/', os.sep) + if len(self.folders) == 1: + listings_folder = next(iter(self.folders.keys())) + if fname.startswith(listings_folder): + fpath = os.path.join(fname) # new syntax: specify folder name + else: + # old syntax: don't specify folder name + fpath = os.path.join(listings_folder, fname) + else: + # must be new syntax: specify folder name + fpath = os.path.join(fname) + linenumbers = 'table' if linenumbers else False + deps = [fpath] + with open(fpath, 'r') as inf: + target = urlunsplit( + ("link", 'listing', fpath.replace('\\', '/'), '', '')) + src_target = urlunsplit( + ("link", 'listing_source', fpath.replace('\\', '/'), '', '')) + src_label = self.site.MESSAGES('Source') + + data = inf.read() + lexer = pygments.lexers.get_lexer_by_name(language) + formatter = pygments.formatters.get_formatter_by_name( + 'html', linenos=linenumbers) + output = '<a href="{1}">{0}</a> <a href="{3}">({2})</a>' .format( + fname, target, src_label, src_target) + pygments.highlight(data, lexer, formatter) + + return output, deps diff --git a/nikola/plugins/shortcode/post_list.plugin b/nikola/plugins/shortcode/post_list.plugin new file mode 100644 index 0000000..494a1d8 --- /dev/null +++ b/nikola/plugins/shortcode/post_list.plugin @@ -0,0 +1,13 @@ +[Core] +name = post_list +module = post_list + +[Nikola] +PluginCategory = Shortcode + +[Documentation] +author = Udo Spallek +version = 0.2 +website = https://getnikola.com/ +description = Includes a list of posts with tag and slice based filters. + diff --git a/nikola/plugins/shortcode/post_list.py b/nikola/plugins/shortcode/post_list.py new file mode 100644 index 0000000..462984a --- /dev/null +++ b/nikola/plugins/shortcode/post_list.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2013-2020 Udo Spallek, Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Post list shortcode.""" + + +import operator +import os +import uuid + +import natsort + +from nikola import utils +from nikola.packages.datecond import date_in_range +from nikola.plugin_categories import ShortcodePlugin + + +class PostListShortcode(ShortcodePlugin): + """Provide a shortcode to create a list of posts. + + Post List + ========= + :Directive Arguments: None. + :Directive Options: lang, start, stop, reverse, sort, date, tags, categories, sections, slugs, post_type, template, id + :Directive Content: None. + + The posts appearing in the list can be filtered by options. + *List slicing* is provided with the *start*, *stop* and *reverse* options. + + The following not required options are recognized: + + ``start`` : integer + The index of the first post to show. + A negative value like ``-3`` will show the *last* three posts in the + post-list. + Defaults to None. + + ``stop`` : integer + The index of the last post to show. + A value negative value like ``-1`` will show every post, but not the + *last* in the post-list. + Defaults to None. + + ``reverse`` : flag + Reverse the order of the post-list. + Defaults is to not reverse the order of posts. + + ``sort`` : string + Sort post list by one of each post's attributes, usually ``title`` or a + custom ``priority``. Defaults to None (chronological sorting). + + ``date`` : string + Show posts that match date range specified by this option. Format: + + * comma-separated clauses (AND) + * clause: attribute comparison_operator value (spaces optional) + * attribute: year, month, day, hour, month, second, weekday, isoweekday; or empty for full datetime + * comparison_operator: == != <= >= < > + * value: integer, 'now', 'today', or dateutil-compatible date input + + ``tags`` : string [, string...] + Filter posts to show only posts having at least one of the ``tags``. + Defaults to None. + + ``require_all_tags`` : flag + Change tag filter behaviour to show only posts that have all specified ``tags``. + Defaults to False. + + ``categories`` : string [, string...] + Filter posts to show only posts having one of the ``categories``. + Defaults to None. + + ``sections`` : string [, string...] + Filter posts to show only posts having one of the ``sections``. + Defaults to None. + + ``slugs`` : string [, string...] + Filter posts to show only posts having at least one of the ``slugs``. + Defaults to None. + + ``post_type`` (or ``type``) : string + Show only ``posts``, ``pages`` or ``all``. + Replaces ``all``. Defaults to ``posts``. + + ``lang`` : string + The language of post *titles* and *links*. + Defaults to default language. + + ``template`` : string + The name of an alternative template to render the post-list. + Defaults to ``post_list_directive.tmpl`` + + ``id`` : string + A manual id for the post list. + Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex. + """ + + name = "post_list" + + def set_site(self, site): + """Set the site.""" + super().set_site(site) + site.register_shortcode('post-list', self.handler) + + def handler(self, start=None, stop=None, reverse=False, tags=None, require_all_tags=False, categories=None, + sections=None, slugs=None, post_type='post', type=False, + lang=None, template='post_list_directive.tmpl', sort=None, + id=None, data=None, state=None, site=None, date=None, filename=None, post=None): + """Generate HTML for post-list.""" + if lang is None: + lang = utils.LocaleBorg().current_lang + if site.invariant: # for testing purposes + post_list_id = id or 'post_list_' + 'fixedvaluethatisnotauuid' + else: + post_list_id = id or 'post_list_' + uuid.uuid4().hex + + # Get post from filename if available + if filename: + self_post = site.post_per_input_file.get(filename) + else: + self_post = None + + if self_post: + self_post.register_depfile("####MAGIC####TIMELINE", lang=lang) + + # If we get strings for start/stop, make them integers + if start is not None: + start = int(start) + if stop is not None: + stop = int(stop) + + # Parse tags/categories/sections/slugs (input is strings) + categories = [c.strip().lower() for c in categories.split(',')] if categories else [] + sections = [s.strip().lower() for s in sections.split(',')] if sections else [] + slugs = [s.strip() for s in slugs.split(',')] if slugs else [] + + filtered_timeline = [] + posts = [] + step = None if reverse is False else -1 + + if type is not False: + post_type = type + + if post_type == 'page' or post_type == 'pages': + timeline = [p for p in site.timeline if not p.use_in_feeds] + elif post_type == 'all': + timeline = [p for p in site.timeline] + else: # post + timeline = [p for p in site.timeline if p.use_in_feeds] + + # self_post should be removed from timeline because this is redundant + timeline = [p for p in timeline if p.source_path != filename] + + if categories: + timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories] + + if sections: + timeline = [p for p in timeline if p.section_name(lang).lower() in sections] + + if tags: + tags = {t.strip().lower() for t in tags.split(',')} + if require_all_tags: + compare = set.issubset + else: + compare = operator.and_ + for post in timeline: + post_tags = {t.lower() for t in post.tags} + if compare(tags, post_tags): + filtered_timeline.append(post) + else: + filtered_timeline = timeline + + if sort: + filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC) + + if date: + _now = utils.current_time() + filtered_timeline = [p for p in filtered_timeline if date_in_range(utils.html_unescape(date), p.date, now=_now)] + + for post in filtered_timeline[start:stop:step]: + if slugs: + cont = True + for slug in slugs: + if slug == post.meta('slug'): + cont = False + + if cont: + continue + + bp = post.translated_base_path(lang) + if os.path.exists(bp) and state: + state.document.settings.record_dependencies.add(bp) + elif os.path.exists(bp) and self_post: + self_post.register_depfile(bp, lang=lang) + + posts += [post] + + template_deps = site.template_system.template_deps(template) + if state: + # Register template as a dependency (Issue #2391) + for d in template_deps: + state.document.settings.record_dependencies.add(d) + elif self_post: + for d in template_deps: + self_post.register_depfile(d, lang=lang) + + template_data = { + 'lang': lang, + 'posts': posts, + # Need to provide str, not TranslatableSetting (Issue #2104) + 'date_format': site.GLOBAL_CONTEXT.get('date_format')[lang], + 'post_list_id': post_list_id, + 'messages': site.MESSAGES, + '_link': site.link, + } + output = site.template_system.render_template( + template, None, template_data) + return output, template_deps + + +# Request file name from shortcode (Issue #2412) +PostListShortcode.handler.nikola_shortcode_pass_filename = True diff --git a/nikola/plugins/shortcode/thumbnail.plugin b/nikola/plugins/shortcode/thumbnail.plugin new file mode 100644 index 0000000..e55d34f --- /dev/null +++ b/nikola/plugins/shortcode/thumbnail.plugin @@ -0,0 +1,12 @@ +[Core] +name = thumbnail +module = thumbnail + +[Nikola] +PluginCategory = Shortcode + +[Documentation] +author = Chris Warrick +version = 0.1 +website = https://getnikola.com/ +description = Thumbnail shortcode diff --git a/nikola/plugins/shortcode/thumbnail.py b/nikola/plugins/shortcode/thumbnail.py new file mode 100644 index 0000000..feb731b --- /dev/null +++ b/nikola/plugins/shortcode/thumbnail.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2017-2020 Roberto Alsina, Chris Warrick 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. + +"""Thumbnail shortcode (equivalent to reST’s thumbnail directive).""" + +import os.path + +from nikola.plugin_categories import ShortcodePlugin + + +class ThumbnailShortcode(ShortcodePlugin): + """Plugin for thumbnail directive.""" + + name = "thumbnail" + + def handler(self, uri, alt=None, align=None, linktitle=None, title=None, imgclass=None, figclass=None, site=None, data=None, lang=None, post=None): + """Create HTML for thumbnail.""" + if uri.endswith('.svg'): + # the ? at the end makes docutil output an <img> instead of an object for the svg, which lightboxes may require + src = '.thumbnail'.join(os.path.splitext(uri)) + '?' + else: + src = '.thumbnail'.join(os.path.splitext(uri)) + + if imgclass is None: + imgclass = '' + if figclass is None: + figclass = '' + + if align and data: + figclass += ' align-{0}'.format(align) + elif align: + imgclass += ' align-{0}'.format(align) + + output = '<a href="{0}" class="image-reference"'.format(uri) + if linktitle: + output += ' title="{0}"'.format(linktitle) + output += '><img src="{0}"'.format(src) + for item, name in ((alt, 'alt'), (title, 'title'), (imgclass, 'class')): + if item: + output += ' {0}="{1}"'.format(name, item) + output += '></a>' + + if data: + output = '<div class="figure {0}">{1}{2}</div>'.format(figclass, output, data) + + return output, [] diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py index 4eeae62..3e18cd5 100644 --- a/nikola/plugins/task/__init__.py +++ b/nikola/plugins/task/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin index eb079da..62e5fd9 100644 --- a/nikola/plugins/task/archive.plugin +++ b/nikola/plugins/task/archive.plugin @@ -1,5 +1,5 @@ [Core] -name = render_archive +name = classify_archive module = archive [Documentation] @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Generates the blog's archive pages. [Nikola] -plugincategory = Task +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index 303d349..4cbf215 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,231 +24,216 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Render the post archives.""" +"""Classify the posts in archives.""" -import copy -import os - -# for tearDown with _reload we cannot use 'import from' to access LocaleBorg -import nikola.utils import datetime -from nikola.plugin_categories import Task -from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name_for_index_link +from collections import defaultdict + +import natsort +import nikola.utils +from nikola.plugin_categories import Taxonomy + + +class Archive(Taxonomy): + """Classify the post archives.""" + + name = "classify_archive" + + classification_name = "archive" + overview_page_variable_name = "archive" + more_than_one_classifications_per_post = False + has_hierarchy = True + include_posts_from_subhierarchies = True + include_posts_into_hierarchy_root = True + subcategories_list_template = "list.tmpl" + template_for_classification_overview = None + always_disable_rss = True + always_disable_atom = True + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = False + add_other_languages_variable = True + path_handler_docstrings = { + 'archive_index': False, + 'archive': """Link to archive path, name is the year. -class Archive(Task): - """Render the post archives.""" + Example: - name = "render_archive" + link://archive/2013 => /archives/2013/index.html""", + 'archive_atom': False, + 'archive_rss': False, + } 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 should be None - # template_name: name of the template to use - # 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 - # Depend on all post metadata because it can be used in templates (Issue #1931) - task_cfg.append([repr(p) for p in posts]) + # Sanity checks + if (site.config['CREATE_MONTHLY_ARCHIVE'] and site.config['CREATE_SINGLE_ARCHIVE']) and not site.config['CREATE_FULL_ARCHIVES']: + raise Exception('Cannot create monthly and single archives at the same time.') + # Finish setup + self.show_list_as_subcategories_list = not site.config['CREATE_FULL_ARCHIVES'] + self.show_list_as_index = site.config['ARCHIVES_ARE_INDEXES'] + self.template_for_single_list = "archiveindex.tmpl" if site.config['ARCHIVES_ARE_INDEXES'] else "archive.tmpl" + # Determine maximum hierarchy height + if site.config['CREATE_DAILY_ARCHIVE'] or site.config['CREATE_FULL_ARCHIVES']: + self.max_levels = 3 + elif site.config['CREATE_MONTHLY_ARCHIVE']: + self.max_levels = 2 + elif site.config['CREATE_SINGLE_ARCHIVE']: + self.max_levels = 0 else: - # Depend on the content of items, to rebuild if links change (Issue #1931) - context["items"] = items - task_cfg.append(items) - task = self.site.generic_post_list_renderer( - lang, - [], - os.path.join(kw['output_folder'], self.site.path("archive", name, lang)), - template_name, - kw['filters'], - context, - ) - - 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')] - task['basename'] = self.name - 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']: - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return adjust_name_for_index_link(self.site.link("archive" + feed, name, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - def page_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return adjust_name_for_index_path(self.site.path("archive" + feed, name, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - 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", - context, - kw, - str(self.name), - page_link, - page_path, - uptodate) + self.max_levels = 1 + return super().set_site(site) + + def get_implicit_classifications(self, lang): + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [''] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + levels = [str(post.date.year).zfill(4), str(post.date.month).zfill(2), str(post.date.day).zfill(2)] + return ['/'.join(levels[:self.max_levels])] + + def sort_classifications(self, classifications, lang, level=None): + """Sort the given list of classification strings.""" + if level in (0, 1): + # Years or months: sort descending + classifications.sort() + classifications.reverse() + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + classification = self.extract_hierarchy(classification) + if len(classification) == 0: + return self.site.MESSAGES[lang]['Archive'] + elif len(classification) == 1: + return classification[0] + elif len(classification) == 2: + if only_last_component: + date_str = "{month}" + else: + date_str = "{month_year}" + return nikola.utils.LocaleBorg().format_date_in_string( + date_str, + datetime.date(int(classification[0]), int(classification[1]), 1), + lang) + else: + if only_last_component: + return str(classification[2]) + return nikola.utils.LocaleBorg().format_date_in_string( + "{month_day_year}", + datetime.date(int(classification[0]), int(classification[1]), int(classification[2])), + lang) + + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" + components = [self.site.config['ARCHIVE_PATH'](lang)] + if classification: + components.extend(classification) + add_index = 'always' else: - yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable) + components.append(self.site.config['ARCHIVE_FILENAME'](lang)) + add_index = 'never' + return [_f for _f in components if _f], add_index + + def extract_hierarchy(self, classification): + """Given a classification, return a list of parts in the hierarchy.""" + return classification.split('/') if classification else [] - def gen_tasks(self): - """Generate archive tasks.""" + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string.""" + return '/'.join(hierarchy) + + def provide_context_and_uptodate(self, classification, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + hierarchy = self.extract_hierarchy(classification) kw = { "messages": self.site.MESSAGES, - "translations": self.site.config['TRANSLATIONS'], - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - "archives_are_indexes": self.site.config['ARCHIVES_ARE_INDEXES'], - "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'], - "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "create_full_archives": self.site.config['CREATE_FULL_ARCHIVES'], - "create_daily_archive": self.site.config['CREATE_DAILY_ARCHIVE'], - "pretty_urls": self.site.config['PRETTY_URLS'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "index_file": self.site.config['INDEX_FILE'], - "generate_atom": self.site.config["GENERATE_ATOM"], } - self.site.scan_posts() - yield self.group_task() - # TODO add next/prev links for years - if (kw['create_monthly_archive'] and kw['create_single_archive']) and not kw['create_full_archives']: - raise Exception('Cannot create monthly and single archives at the same time.') - for lang in kw["translations"]: - if kw['create_single_archive'] and not kw['create_full_archives']: - # if we are creating one single archive - archdata = {} - else: - # if we are not creating one single archive, start with all years - archdata = self.site.posts_per_year.copy() - if kw['create_single_archive'] or kw['create_full_archives']: - # if we are creating one single archive, or full archives - archdata[None] = self.site.posts # for create_single_archive - - for year, posts in archdata.items(): - # Filter untranslated posts (Issue #1360) - if not kw["show_untranslated_posts"]: - posts = [p for p in posts if lang in p.translated_to] - - # Add archive per year or total archive - if year: - title = kw["messages"][lang]["Posts for year %s"] % year - kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != year) - else: - title = kw["messages"][lang]["Archive"] - kw["is_feed_stale"] = False - deps_translatable = {} - for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: - deps_translatable[k] = self.site.GLOBAL_CONTEXT[k](lang) - if not kw["create_monthly_archive"] or kw["create_full_archives"]: - yield self._generate_posts_task(kw, year, lang, posts, title, deps_translatable) - else: - months = set([(m.split('/')[1], self.site.link("archive", m, lang), len(self.site.posts_per_month[m])) for m in self.site.posts_per_month.keys() if m.startswith(str(year))]) - months = sorted(list(months)) - months.reverse() - items = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link, count] for month, link, count in months] - yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable) - - if not kw["create_monthly_archive"] and not kw["create_full_archives"] and not kw["create_daily_archive"]: - continue # Just to avoid nesting the other loop in this if - for yearmonth, posts in self.site.posts_per_month.items(): - # Add archive per month - year, month = yearmonth.split('/') - - kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m") != yearmonth) - - # Filter untranslated posts (via Issue #1360) - if not kw["show_untranslated_posts"]: - posts = [p for p in posts if lang in p.translated_to] - - if kw["create_monthly_archive"] or kw["create_full_archives"]: - title = kw["messages"][lang]["Posts for {month} {year}"].format( - year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang)) - yield self._generate_posts_task(kw, yearmonth, lang, posts, title) - - if not kw["create_full_archives"] and not kw["create_daily_archive"]: - continue # Just to avoid nesting the other loop in this if - # Add archive per day - days = dict() - for p in posts: - if p.date.day not in days: - days[p.date.day] = list() - days[p.date.day].append(p) - for day, posts in days.items(): - title = kw["messages"][lang]["Posts for {month} {day}, {year}"].format( - year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang), day=day) - yield self._generate_posts_task(kw, yearmonth + '/{0:02d}'.format(day), lang, posts, title) - - if not kw['create_single_archive'] and not kw['create_full_archives']: - # And an "all your years" page for yearly and monthly archives - if "is_feed_stale" in kw: - del kw["is_feed_stale"] - years = list(self.site.posts_per_year.keys()) - years.sort(reverse=True) - kw['years'] = years - for lang in kw["translations"]: - items = [(y, self.site.link("archive", y, lang), len(self.site.posts_per_year[y])) for y in years] - yield self._prepare_task(kw, None, lang, None, items, "list.tmpl", kw["messages"][lang]["Archive"]) - - def archive_path(self, name, lang, is_feed=False): - """Link to archive path, name is the year. - - Example: - - link://archive/2013 => /archives/2013/index.html - """ - if is_feed: - extension = ".atom" - archive_file = os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0] + extension - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension - else: - archive_file = self.site.config['ARCHIVE_FILENAME'] - index_file = self.site.config['INDEX_FILE'] - if name: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['ARCHIVE_PATH'], name, - index_file] if _f] + page_kind = "list" + if self.show_list_as_index: + if not self.show_list_as_subcategories_list or len(hierarchy) == self.max_levels: + page_kind = "index" + if len(hierarchy) == 0: + title = kw["messages"][lang]["Archive"] + elif len(hierarchy) == 1: + title = kw["messages"][lang]["Posts for year %s"] % hierarchy[0] + elif len(hierarchy) == 2: + title = nikola.utils.LocaleBorg().format_date_in_string( + kw["messages"][lang]["Posts for {month_year}"], + datetime.date(int(hierarchy[0]), int(hierarchy[1]), 1), + lang) + elif len(hierarchy) == 3: + title = nikola.utils.LocaleBorg().format_date_in_string( + kw["messages"][lang]["Posts for {month_day_year}"], + datetime.date(int(hierarchy[0]), int(hierarchy[1]), int(hierarchy[2])), + lang) else: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['ARCHIVE_PATH'], - archive_file] if _f] + raise Exception("Cannot interpret classification {}!".format(repr(classification))) - def archive_atom_path(self, name, lang): - """Link to atom archive path, name is the year. - - Example: + context = { + "title": title, + "pagekind": [page_kind, "archive_page"], + "create_archive_navigation": self.site.config["CREATE_ARCHIVE_NAVIGATION"], + "archive_name": classification + } - link://archive_atom/2013 => /archives/2013/index.atom - """ - return self.archive_path(name, lang, is_feed=True) + # Generate links for hierarchies + if context["create_archive_navigation"]: + if hierarchy: + # Up level link makes sense only if this is not the top-level + # page (hierarchy is empty) + parent = '/'.join(hierarchy[:-1]) + context["up_archive"] = self.site.link('archive', parent, lang) + context["up_archive_name"] = self.get_classification_friendly_name(parent, lang) + else: + context["up_archive"] = None + context["up_archive_name"] = None + + nodelevel = len(hierarchy) + flat_samelevel = self.archive_navigation[lang][nodelevel] + idx = flat_samelevel.index(classification) + if idx == -1: + raise Exception("Cannot find classification {0} in flat hierarchy!".format(classification)) + previdx, nextidx = idx - 1, idx + 1 + # If the previous index is -1, or the next index is 1, the previous/next archive does not exist. + context["previous_archive"] = self.site.link('archive', flat_samelevel[previdx], lang) if previdx != -1 else None + context["previous_archive_name"] = self.get_classification_friendly_name(flat_samelevel[previdx], lang) if previdx != -1 else None + context["next_archive"] = self.site.link('archive', flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None + context["next_archive_name"] = self.get_classification_friendly_name(flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None + context["archive_nodelevel"] = nodelevel + context["has_archive_navigation"] = bool(context["previous_archive"] or context["up_archive"] or context["next_archive"]) + else: + context["has_archive_navigation"] = False + kw.update(context) + return context, kw + + def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + # Build a lookup table for archive navigation, if we’ll need one. + if self.site.config['CREATE_ARCHIVE_NAVIGATION']: + if flat_hierarchy_per_lang is None: + raise ValueError('Archives need flat_hierarchy_per_lang') + self.archive_navigation = {} + for lang, flat_hierarchy in flat_hierarchy_per_lang.items(): + self.archive_navigation[lang] = defaultdict(list) + for node in flat_hierarchy: + if not self.site.config["SHOW_UNTRANSLATED_POSTS"]: + if not [x for x in posts_per_classification_per_language[lang][node.classification_name] if x.is_translation_available(lang)]: + continue + self.archive_navigation[lang][len(node.classification_path)].append(node.classification_name) + + # We need to sort it. Natsort means it’s year 10000 compatible! + for k, v in self.archive_navigation[lang].items(): + self.archive_navigation[lang][k] = natsort.natsorted(v, alg=natsort.ns.F | natsort.ns.IC) + + return super().postprocess_posts_per_classification(posts_per_classification_per_language, flat_hierarchy_per_lang, hierarchy_lookup_per_lang) + + def should_generate_classification_page(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + return classification == '' or len(post_list) > 0 + + def get_other_language_variants(self, classification, lang, classifications_per_language): + """Return a list of variants of the same classification in other languages.""" + return [(other_lang, classification) for other_lang, lookup in classifications_per_language.items() if classification in lookup and other_lang != lang] diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin index 3fc4ef2..19e687c 100644 --- a/nikola/plugins/task/authors.plugin +++ b/nikola/plugins/task/authors.plugin @@ -1,5 +1,5 @@ [Core] -Name = render_authors +Name = classify_authors Module = authors [Documentation] @@ -8,3 +8,5 @@ Version = 0.1 Website = http://getnikola.com Description = Render the author pages and feeds. +[Nikola] +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index ec61800..24fe650 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2015-2016 Juanjo Conti and others. +# Copyright © 2015-2020 Juanjo Conti and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,301 +26,134 @@ """Render the author pages and feeds.""" -from __future__ import unicode_literals -import os -import natsort -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA -from collections import defaultdict -from blinker import signal - -from nikola.plugin_categories import Task +from nikola.plugin_categories import Taxonomy from nikola import utils -class RenderAuthors(Task): - """Render the author pages and feeds.""" - - name = "render_authors" - posts_per_author = None - - def set_site(self, site): - """Set Nikola site.""" - self.generate_author_pages = False - if site.config["ENABLE_AUTHOR_PAGES"]: - site.register_path_handler('author_index', self.author_index_path) - site.register_path_handler('author', self.author_path) - site.register_path_handler('author_atom', self.author_atom_path) - site.register_path_handler('author_rss', self.author_rss_path) - signal('scanned').connect(self.posts_scanned) - return super(RenderAuthors, self).set_site(site) - - def posts_scanned(self, event): - """Called after posts are scanned via signal.""" - self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and len(self._posts_per_author()) > 1 - self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages - - def gen_tasks(self): - """Render the author pages and feeds.""" - kw = { - "translations": self.site.config["TRANSLATIONS"], - "blog_title": self.site.config["BLOG_TITLE"], - "site_url": self.site.config["SITE_URL"], - "base_url": self.site.config["BASE_URL"], - "messages": self.site.MESSAGES, - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - 'author_path': self.site.config['AUTHOR_PATH'], - "author_pages_are_indexes": self.site.config['AUTHOR_PAGES_ARE_INDEXES'], - "generate_rss": self.site.config['GENERATE_RSS'], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "feed_length": self.site.config['FEED_LENGTH'], - "tzinfo": self.site.tzinfo, - "pretty_urls": self.site.config['PRETTY_URLS'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "index_file": self.site.config['INDEX_FILE'], - } - - self.site.scan_posts() - yield self.group_task() - - if self.generate_author_pages: - yield self.list_authors_page(kw) - - if not self._posts_per_author(): # this may be self.site.posts_per_author - return - - author_list = list(self._posts_per_author().items()) +class ClassifyAuthors(Taxonomy): + """Classify the posts by authors.""" - def render_lists(author, posts): - """Render author pages as RSS files and lists/indexes.""" - post_list = sorted(posts, key=lambda a: a.date) - post_list.reverse() - for lang in kw["translations"]: - if kw["show_untranslated_posts"]: - filtered_posts = post_list - else: - filtered_posts = [x for x in post_list if x.is_translation_available(lang)] - if kw["generate_rss"]: - yield self.author_rss(author, lang, filtered_posts, kw) - # Render HTML - if kw['author_pages_are_indexes']: - yield self.author_page_as_index(author, lang, filtered_posts, kw) - else: - yield self.author_page_as_list(author, lang, filtered_posts, kw) + name = "classify_authors" - for author, posts in author_list: - for task in render_lists(author, posts): - yield task + classification_name = "author" + overview_page_variable_name = "authors" + more_than_one_classifications_per_post = False + has_hierarchy = False + template_for_classification_overview = "authors.tmpl" + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = False + add_other_languages_variable = True + path_handler_docstrings = { + 'author_index': """ Link to the authors index. - def _create_authors_page(self, kw): - """Create a global "all authors" page for each language.""" - template_name = "authors.tmpl" - kw = kw.copy() - for lang in kw["translations"]: - authors = natsort.natsorted([author for author in self._posts_per_author().keys()], - alg=natsort.ns.F | natsort.ns.IC) - has_authors = (authors != []) - kw['authors'] = authors - output_name = os.path.join( - kw['output_folder'], self.site.path('author_index', None, lang)) - context = {} - if has_authors: - context["title"] = kw["messages"][lang]["Authors"] - context["items"] = [(author, self.site.link("author", author, lang)) for author - in authors] - context["description"] = context["title"] - else: - context["items"] = None - context["permalink"] = self.site.link("author_index", None, lang) - context["pagekind"] = ["list", "authors_page"] - task = self.site.generic_post_list_renderer( - lang, - [], - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:page')] - task['basename'] = str(self.name) - yield task + Example: - def list_authors_page(self, kw): - """Create a global "all authors" page for each language.""" - yield self._create_authors_page(kw) + link://authors/ => /authors/index.html""", + 'author': """Link to an author's page. - def _get_title(self, author): - return author + Example: - def _get_description(self, author, lang): - descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS'] - return descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None + link://author/joe => /authors/joe.html""", + 'author_atom': """Link to an author's Atom feed. - def author_page_as_index(self, author, lang, post_list, kw): - """Render a sort of index page collection using only this author's posts.""" - kind = "author" +Example: - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension) +link://author_atom/joe => /authors/joe.atom""", + 'author_rss': """Link to an author's RSS feed. - def page_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension) +Example: - context_source = {} - title = self._get_title(author) - if kw["generate_rss"]: - # On a author page, the feeds include the author's feeds - rss_link = ("""<link rel="alternate" type="application/rss+xml" """ - """title="RSS for author """ - """{0} ({1})" href="{2}">""".format( - title, lang, self.site.link(kind + "_rss", author, lang))) - context_source['rss_link'] = rss_link - context_source["author"] = title - indexes_title = kw["messages"][lang]["Posts by %s"] % title - context_source["description"] = self._get_description(author, lang) - context_source["pagekind"] = ["index", "author_page"] - template_name = "authorindex.tmpl" +link://author_rss/joe => /authors/joe.xml""", + } - yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path) + def set_site(self, site): + """Set Nikola site.""" + super().set_site(site) + self.show_list_as_index = site.config['AUTHOR_PAGES_ARE_INDEXES'] + self.more_than_one_classifications_per_post = site.config.get('MULTIPLE_AUTHORS_PER_POST', False) + self.template_for_single_list = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl" + self.translation_manager = utils.ClassificationTranslationManager() + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + if not self.site.config["ENABLE_AUTHOR_PAGES"]: + return False + if lang is not None: + return self.generate_author_pages + return True + + def classify(self, post, lang): + """Classify the given post for the given language.""" + if self.more_than_one_classifications_per_post: + return post.authors(lang=lang) + else: + return [post.author(lang=lang)] - def author_page_as_list(self, author, lang, post_list, kw): - """Render a single flat link list with this author's posts.""" - kind = "author" - template_name = "author.tmpl" - output_name = os.path.join(kw['output_folder'], self.site.path( - kind, author, lang)) - context = {} - context["lang"] = lang - title = self._get_title(author) - context["author"] = title - context["title"] = kw["messages"][lang]["Posts by %s"] % title - context["posts"] = post_list - context["permalink"] = self.site.link(kind, author, lang) - context["kind"] = kind - context["description"] = self._get_description(author, lang) - context["pagekind"] = ["list", "author_page"] - task = self.site.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:list')] - task['basename'] = str(self.name) - yield task + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return classification - def author_rss(self, author, lang, posts, kw): - """Create a RSS feed for a single author in a given language.""" - kind = "author" - # Render RSS - output_name = os.path.normpath( - os.path.join(kw['output_folder'], - self.site.path(kind + "_rss", author, lang))) - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", author, lang).lstrip('/')) - deps = [] - deps_uptodate = [] - post_list = sorted(posts, key=lambda a: a.date) - post_list.reverse() - for post in post_list: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) - task = { - 'basename': str(self.name), - 'name': output_name, - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(author)), - kw["site_url"], None, post_list, - output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], - feed_url, None, kw["feed_link_append_query"]))], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.authors:rss')] + deps_uptodate, - 'task_dep': ['render_posts'], - } - return utils.apply_filters(task, kw['filters']) + def get_overview_path(self, lang, dest_type='page'): + """Return a path for the list of all classifications.""" + path = self.site.config['AUTHOR_PATH'](lang) + return [component for component in path.split('/') if component], 'always' - def slugify_author_name(self, name, lang=None): - """Slugify an author name.""" - if lang is None: # TODO: remove in v8 - utils.LOGGER.warn("RenderAuthors.slugify_author_name() called without language!") - lang = '' + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" if self.site.config['SLUG_AUTHOR_PATH']: - name = utils.slugify(name, lang) - return name - - def author_index_path(self, name, lang): - """Link to the author's index. - - Example: - - link://authors/ => /authors/index.html - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.site.config['INDEX_FILE']] if _f] - - def author_path(self, name, lang): - """Link to an author's page. - - Example: - - link://author/joe => /authors/joe.html - """ - if self.site.config['PRETTY_URLS']: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.slugify_author_name(name, lang), - self.site.config['INDEX_FILE']] if _f] + slug = utils.slugify(classification, lang) else: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.slugify_author_name(name, lang) + ".html"] if _f] - - def author_atom_path(self, name, lang): - """Link to an author's Atom feed. - - Example: - - link://author_atom/joe => /authors/joe.atom - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".atom"] if - _f] - - def author_rss_path(self, name, lang): - """Link to an author's RSS feed. - - Example: - - link://author_rss/joe => /authors/joe.rss - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".xml"] if - _f] + slug = classification + return [self.site.config['AUTHOR_PATH'](lang), slug], 'auto' - def _add_extension(self, path, extension): - path[-1] += extension - return path + def provide_overview_context_and_uptodate(self, lang): + """Provide data for the context and the uptodate list for the list of all classifiations.""" + kw = { + "messages": self.site.MESSAGES, + } + context = { + "title": kw["messages"][lang]["Authors"], + "description": kw["messages"][lang]["Authors"], + "permalink": self.site.link("author_index", None, lang), + "pagekind": ["list", "authors_page"], + } + kw.update(context) + return context, kw - def _posts_per_author(self): - """Return a dict of posts per author.""" - if self.posts_per_author is None: - self.posts_per_author = defaultdict(list) - for post in self.site.timeline: - if post.is_post: - self.posts_per_author[post.author()].append(post) - return self.posts_per_author + def provide_context_and_uptodate(self, classification, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS'] + kw = { + "messages": self.site.MESSAGES, + } + context = { + "author": classification, + "title": kw["messages"][lang]["Posts by %s"] % classification, + "description": descriptions[lang][classification] if lang in descriptions and classification in descriptions[lang] else None, + "pagekind": ["index" if self.show_list_as_index else "list", "author_page"], + } + kw.update(context) + return context, kw + + def get_other_language_variants(self, classification, lang, classifications_per_language): + """Return a list of variants of the same author in other languages.""" + return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language) + + def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + more_than_one = False + for lang, posts_per_author in posts_per_classification_per_language.items(): + authors = set() + for author, posts in posts_per_author.items(): + for post in posts: + if not self.site.config["SHOW_UNTRANSLATED_POSTS"] and not post.is_translation_available(lang): + continue + authors.add(author) + if len(authors) > 1: + more_than_one = True + self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one + self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages + self.translation_manager.add_defaults(posts_per_classification_per_language) diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin index b5bf6e4..939065b 100644 --- a/nikola/plugins/task/bundles.plugin +++ b/nikola/plugins/task/bundles.plugin @@ -6,8 +6,8 @@ module = bundles author = Roberto Alsina version = 1.0 website = https://getnikola.com/ -description = Theme bundles using WebAssets +description = Bundle assets [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py index b33d8e0..aa4ce78 100644 --- a/nikola/plugins/task/bundles.py +++ b/nikola/plugins/task/bundles.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,38 +24,26 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Bundle assets using WebAssets.""" +"""Bundle assets.""" -from __future__ import unicode_literals +import configparser +import io +import itertools import os - -try: - import webassets -except ImportError: - webassets = None # NOQA +import shutil from nikola.plugin_categories import LateTask from nikola import utils class BuildBundles(LateTask): - """Bundle assets using WebAssets.""" + """Bundle assets.""" name = "create_bundles" - def set_site(self, site): - """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.') - site.config['USE_BUNDLES'] = False - site._GLOBAL_CONTEXT['use_bundles'] = False - super(BuildBundles, self).set_site(site) - def gen_tasks(self): - """Bundle assets using WebAssets.""" + """Bundle assets.""" kw = { 'filters': self.site.config['FILTERS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], @@ -69,28 +57,21 @@ class BuildBundles(LateTask): def build_bundle(output, inputs): out_dir = os.path.join(kw['output_folder'], os.path.dirname(output)) - inputs = [os.path.relpath(i, out_dir) for i in inputs if os.path.isfile(i)] - cache_dir = os.path.join(kw['cache_folder'], 'webassets') - utils.makedirs(cache_dir) - env = webassets.Environment(out_dir, os.path.dirname(output), - cache=cache_dir) - if inputs: - bundle = webassets.Bundle(*inputs, output=os.path.basename(output)) - env.register(output, bundle) - # This generates the file - try: - env[output].urls() - except Exception as e: - self.logger.error("Failed to build bundles.") - self.logger.exception(e) - self.logger.notice("Try running ``nikola clean`` and building again.") - else: - with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'): - pass # Create empty file + inputs = [ + os.path.join( + out_dir, + os.path.relpath(i, out_dir)) + for i in inputs if os.path.isfile(i) + ] + with open(os.path.join(out_dir, os.path.basename(output)), 'wb+') as out_fh: + for i in inputs: + with open(i, 'rb') as in_fh: + shutil.copyfileobj(in_fh, out_fh) + out_fh.write(b'\n') yield self.group_task() - if (webassets is not None and self.site.config['USE_BUNDLES'] is not - False): + + if self.site.config['USE_BUNDLES']: for name, _files in kw['theme_bundles'].items(): output_path = os.path.join(kw['output_folder'], name) dname = os.path.dirname(name) @@ -127,19 +108,17 @@ class BuildBundles(LateTask): def get_theme_bundles(themes): """Given a theme chain, return the bundle definitions.""" - bundles = {} for theme_name in themes: bundles_path = os.path.join( utils.get_theme_path(theme_name), 'bundles') if os.path.isfile(bundles_path): - with open(bundles_path) as fd: - for line in fd: - try: - name, files = line.split('=') - files = [f.strip() for f in files.split(',')] - bundles[name.strip().replace('/', os.sep)] = files - except ValueError: - # for empty lines - pass - break - return bundles + config = configparser.ConfigParser() + header = io.StringIO('[bundles]\n') + with open(bundles_path, 'rt') as fd: + config.read_file(itertools.chain(header, fd)) + bundles = {} + for name, files in config['bundles'].items(): + name = name.strip().replace('/', os.sep) + files = [f.strip() for f in files.split(',') if f.strip()] + bundles[name] = files + return bundles diff --git a/nikola/plugins/task/categories.plugin b/nikola/plugins/task/categories.plugin new file mode 100644 index 0000000..be2bb79 --- /dev/null +++ b/nikola/plugins/task/categories.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_categories +module = categories + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Render the category pages and feeds. + +[Nikola] +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/categories.py b/nikola/plugins/task/categories.py new file mode 100644 index 0000000..68f9caa --- /dev/null +++ b/nikola/plugins/task/categories.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the category pages and feeds.""" + +import os + +from nikola.plugin_categories import Taxonomy +from nikola import utils, hierarchy_utils + + +class ClassifyCategories(Taxonomy): + """Classify the posts by categories.""" + + name = "classify_categories" + + classification_name = "category" + overview_page_variable_name = "categories" + overview_page_items_variable_name = "cat_items" + overview_page_hierarchy_variable_name = "cat_hierarchy" + more_than_one_classifications_per_post = False + has_hierarchy = True + include_posts_from_subhierarchies = True + include_posts_into_hierarchy_root = False + show_list_as_subcategories_list = False + template_for_classification_overview = "tags.tmpl" + always_disable_rss = False + always_disable_atom = False + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = True + add_other_languages_variable = True + path_handler_docstrings = { + 'category_index': """A link to the category index. + +Example: + +link://category_index => /categories/index.html""", + 'category': """A link to a category. Takes page number as optional keyword argument. + +Example: + +link://category/dogs => /categories/dogs.html""", + 'category_atom': """A link to a category's Atom feed. + +Example: + +link://category_atom/dogs => /categories/dogs.atom""", + 'category_rss': """A link to a category's RSS feed. + +Example: + +link://category_rss/dogs => /categories/dogs.xml""", + } + + def set_site(self, site): + """Set site, which is a Nikola instance.""" + super().set_site(site) + self.show_list_as_index = self.site.config['CATEGORY_PAGES_ARE_INDEXES'] + self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl" + self.translation_manager = utils.ClassificationTranslationManager() + + # Needed to undo names for CATEGORY_PAGES_FOLLOW_DESTPATH + self.destpath_names_reverse = {} + for lang in self.site.config['TRANSLATIONS']: + self.destpath_names_reverse[lang] = {} + for k, v in self.site.config['CATEGORY_DESTPATH_NAMES'](lang).items(): + self.destpath_names_reverse[lang][v] = k + self.destpath_names_reverse = utils.TranslatableSetting( + '_CATEGORY_DESTPATH_NAMES_REVERSE', self.destpath_names_reverse, + self.site.config['TRANSLATIONS']) + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + return True + + def classify(self, post, lang): + """Classify the given post for the given language.""" + cat = post.meta('category', lang=lang).strip() + return [cat] if cat else [] + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + classification = self.extract_hierarchy(classification) + return classification[-1] if classification else '' + + def get_overview_path(self, lang, dest_type='page'): + """Return a path for the list of all classifications.""" + if self.site.config['CATEGORIES_INDEX_PATH'](lang): + path = self.site.config['CATEGORIES_INDEX_PATH'](lang) + append_index = 'never' + else: + path = self.site.config['CATEGORY_PATH'](lang) + append_index = 'always' + return [component for component in path.split('/') if component], append_index + + def slugify_tag_name(self, name, lang): + """Slugify a tag name.""" + if self.site.config['SLUG_TAG_PATH']: + name = utils.slugify(name, lang) + return name + + def slugify_category_name(self, path, lang): + """Slugify a category name.""" + if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']: + path = path[-1:] # only the leaf + result = [self.slugify_tag_name(part, lang) for part in path] + result[0] = self.site.config['CATEGORY_PREFIX'] + result[0] + if not self.site.config['PRETTY_URLS']: + result = ['-'.join(result)] + return result + + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" + cat_string = '/'.join(classification) + classification_raw = classification # needed to undo CATEGORY_DESTPATH_NAMES + destpath_names_reverse = self.destpath_names_reverse(lang) + if self.site.config['CATEGORY_PAGES_FOLLOW_DESTPATH']: + base_dir = None + for post in self.site.posts_per_category[cat_string]: + if post.category_from_destpath: + base_dir = post.folder_base(lang) + # Handle CATEGORY_DESTPATH_NAMES + if cat_string in destpath_names_reverse: + cat_string = destpath_names_reverse[cat_string] + classification_raw = cat_string.split('/') + break + + if not self.site.config['CATEGORY_DESTPATH_TRIM_PREFIX']: + # If prefixes are not trimmed, we'll already have the base_dir in classification_raw + base_dir = '' + + if base_dir is None: + # fallback: first POSTS entry + classification + base_dir = self.site.config['POSTS'][0][1] + base_dir_list = base_dir.split(os.sep) + sub_dir = [self.slugify_tag_name(part, lang) for part in classification_raw] + return [_f for _f in (base_dir_list + sub_dir) if _f], 'auto' + else: + return [_f for _f in [self.site.config['CATEGORY_PATH'](lang)] if _f] + self.slugify_category_name( + classification, lang), 'auto' + + def extract_hierarchy(self, classification): + """Given a classification, return a list of parts in the hierarchy.""" + return hierarchy_utils.parse_escaped_hierarchical_category_name(classification) + + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string.""" + return hierarchy_utils.join_hierarchical_category_path(hierarchy) + + def provide_overview_context_and_uptodate(self, lang): + """Provide data for the context and the uptodate list for the list of all classifiations.""" + kw = { + 'category_path': self.site.config['CATEGORY_PATH'], + 'category_prefix': self.site.config['CATEGORY_PREFIX'], + "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'], + "tzinfo": self.site.tzinfo, + "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'], + "category_titles": self.site.config['CATEGORY_TITLES'], + } + context = { + "title": self.site.MESSAGES[lang]["Categories"], + "description": self.site.MESSAGES[lang]["Categories"], + "pagekind": ["list", "tags_page"], + } + kw.update(context) + return context, kw + + def provide_context_and_uptodate(self, classification, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + cat_path = self.extract_hierarchy(classification) + kw = { + 'category_path': self.site.config['CATEGORY_PATH'], + 'category_prefix': self.site.config['CATEGORY_PREFIX'], + "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'], + "tzinfo": self.site.tzinfo, + "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'], + "category_titles": self.site.config['CATEGORY_TITLES'], + } + posts = self.site.posts_per_classification[self.classification_name][lang] + if node is None: + children = [] + else: + children = [child for child in node.children if len([post for post in posts.get(child.classification_name, []) if self.site.config['SHOW_UNTRANSLATED_POSTS'] or post.is_translation_available(lang)]) > 0] + subcats = [(child.name, self.site.link(self.classification_name, child.classification_name, lang)) for child in children] + friendly_name = self.get_classification_friendly_name(classification, lang) + context = { + "title": self.site.config['CATEGORY_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % friendly_name), + "description": self.site.config['CATEGORY_DESCRIPTIONS'].get(lang, {}).get(classification), + "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"], + "tag": friendly_name, + "category": classification, + "category_path": cat_path, + "subcategories": subcats, + } + kw.update(context) + return context, kw + + def get_other_language_variants(self, classification, lang, classifications_per_language): + """Return a list of variants of the same category in other languages.""" + return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language) + + def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + self.translation_manager.read_from_config(self.site, 'CATEGORY', posts_per_classification_per_language, False) + + def should_generate_classification_page(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + if self.site.config["CATEGORY_PAGES_FOLLOW_DESTPATH"]: + # In destpath mode, allow users to replace the default category index with a custom page. + classification_hierarchy = self.extract_hierarchy(classification) + dest_list, _ = self.get_path(classification_hierarchy, lang) + short_destination = os.sep.join(dest_list + [self.site.config["INDEX_FILE"]]) + if short_destination in self.site.post_per_file: + return False + return True + + def should_generate_atom_for_classification_page(self, classification, post_list, lang): + """Only generates Atom feed for list of posts for classification if this function returns True.""" + return True + + def should_generate_rss_for_classification_page(self, classification, post_list, lang): + """Only generates RSS feed for list of posts for classification if this function returns True.""" + return True diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin index ddd38df..b63581d 100644 --- a/nikola/plugins/task/copy_assets.plugin +++ b/nikola/plugins/task/copy_assets.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Copy theme assets into output. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py index 4ed7414..c6d32c7 100644 --- a/nikola/plugins/task/copy_assets.py +++ b/nikola/plugins/task/copy_assets.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,11 +26,11 @@ """Copy theme assets into output.""" -from __future__ import unicode_literals import io import os +from nikola.packages.pygments_better_html import BetterHtmlFormatter from nikola.plugin_categories import Task from nikola import utils @@ -48,13 +48,19 @@ class CopyAssets(Task): """ kw = { "themes": self.site.THEMES, + "translations": self.site.translations, "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], - "code.css_selectors": 'pre.code', + "code.css_selectors": ['pre.code', '.code .codetable', '.highlight pre'], + "code.css_wrappers": ['.highlight', '.code'], "code.css_head": '/* code.css file generated by Nikola */\n', - "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", + "code.css_close": ( + "\ntable.codetable, table.highlighttable { width: 100%;}\n" + ".codetable td.linenos, td.linenos { text-align: right; width: 3.5em; " + "padding-right: 0.5em; background: rgba(127, 127, 127, 0.2) }\n" + ".codetable td.code, td.code { padding-left: 0.5em; }\n"), } tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') @@ -63,11 +69,20 @@ class CopyAssets(Task): files_folders=kw['files_folders'], output_dir=None) yield self.group_task() + main_theme = utils.get_theme_path(kw['themes'][0]) + theme_ini = utils.parse_theme_meta(main_theme) + if theme_ini: + ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',') + ignored_assets = [os.path.normpath(asset_name.strip()) for asset_name in ignored_assets] + else: + ignored_assets = [] + for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), 'assets') dst = os.path.join(kw['output_folder'], 'assets') for task in utils.copy_tree(src, dst): - if task['name'] in tasks: + asset_name = os.path.relpath(task['name'], dst) + if task['name'] in tasks or asset_name in ignored_assets: continue tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')] @@ -79,18 +94,18 @@ class CopyAssets(Task): yield utils.apply_filters(task, kw['filters']) # Check whether or not there is a code.css file around. - if not code_css_input: + if not code_css_input and kw['code_color_scheme']: def create_code_css(): - from pygments.formatters import get_formatter_by_name - formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) + formatter = BetterHtmlFormatter(style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) - with io.open(code_css_path, 'w+', encoding='utf8') as outf: + with io.open(code_css_path, 'w+', encoding='utf-8') as outf: outf.write(kw["code.css_head"]) - outf.write(formatter.get_style_defs(kw["code.css_selectors"])) + outf.write(formatter.get_style_defs( + kw["code.css_selectors"], kw["code.css_wrappers"])) outf.write(kw["code.css_close"]) if os.path.exists(code_css_path): - with io.open(code_css_path, 'r', encoding='utf-8') as fh: + with io.open(code_css_path, 'r', encoding='utf-8-sig') as fh: testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"] else: testcontents = False diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin index e4bb1cf..45c2253 100644 --- a/nikola/plugins/task/copy_files.plugin +++ b/nikola/plugins/task/copy_files.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Copy static files into the output. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py index 6f6cfb8..26364d4 100644 --- a/nikola/plugins/task/copy_files.py +++ b/nikola/plugins/task/copy_files.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin index 2064e68..d06e117 100644 --- a/nikola/plugins/task/galleries.plugin +++ b/nikola/plugins/task/galleries.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create image galleries automatically. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index edfd33d..b8ac9ee 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,32 +26,29 @@ """Render image galleries.""" -from __future__ import unicode_literals import datetime import glob import io import json import mimetypes import os -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA +from collections import OrderedDict +from urllib.parse import urljoin import natsort -try: - from PIL import Image # NOQA -except ImportError: - import Image as _Image - Image = _Image - import PyRSS2Gen as rss +from PIL import Image from nikola.plugin_categories import Task from nikola import utils from nikola.image_processing import ImageProcessor from nikola.post import Post +try: + from ruamel.yaml import YAML +except ImportError: + YAML = None + _image_size_cache = {} @@ -63,12 +60,11 @@ class Galleries(Task, ImageProcessor): def set_site(self, site): """Set Nikola site.""" + super().set_site(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', utils.STDERR_HANDLER) - self.kw = { 'thumbnail_size': site.config['THUMBNAIL_SIZE'], 'max_image_size': site.config['MAX_IMAGE_SIZE'], @@ -87,6 +83,11 @@ class Galleries(Task, ImageProcessor): 'generate_rss': site.config['GENERATE_RSS'], 'preserve_exif_data': site.config['PRESERVE_EXIF_DATA'], 'exif_whitelist': site.config['EXIF_WHITELIST'], + 'preserve_icc_profiles': site.config['PRESERVE_ICC_PROFILES'], + 'index_path': site.config['INDEX_PATH'], + 'disable_indexes': site.config['DISABLE_INDEXES'], + 'galleries_use_thumbnail': site.config['GALLERIES_USE_THUMBNAIL'], + 'galleries_default_thumbnail': site.config['GALLERIES_DEFAULT_THUMBNAIL'], } # Verify that no folder in GALLERY_FOLDERS appears twice @@ -104,8 +105,6 @@ class Galleries(Task, ImageProcessor): # Create self.gallery_links self.create_galleries_paths() - return super(Galleries, self).set_site(site) - def _find_gallery_path(self, name): # The system using self.proper_gallery_links and self.improper_gallery_links # is similar as in listings.py. @@ -165,7 +164,7 @@ class Galleries(Task, ImageProcessor): gallery_path = self._find_gallery_path(name) return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + gallery_path.split(os.sep) + - ['rss.xml'] if _f] + [self.site.config['RSS_FILENAME_BASE'](lang) + self.site.config['RSS_EXTENSION']] if _f] def gen_tasks(self): """Render image galleries.""" @@ -173,7 +172,7 @@ class Galleries(Task, ImageProcessor): self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', [])) for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): - self.kw['||template_hooks|{0}||'.format(k)] = v._items + self.kw['||template_hooks|{0}||'.format(k)] = v.calculate_deps() self.site.scan_posts() yield self.group_task() @@ -223,6 +222,12 @@ class Galleries(Task, ImageProcessor): self.kw[k] = self.site.GLOBAL_CONTEXT[k](lang) context = {} + + # Do we have a metadata file? + meta_path, order, captions, img_metadata = self.find_metadata(gallery, lang) + context['meta_path'] = meta_path + context['order'] = order + context['captions'] = captions context["lang"] = lang if post: context["title"] = post.title(lang) @@ -232,7 +237,20 @@ class Galleries(Task, ImageProcessor): image_name_list = [os.path.basename(p) for p in image_list] - if self.kw['use_filename_as_title']: + if captions: + img_titles = [] + for fn in image_name_list: + if fn in captions: + img_titles.append(captions[fn]) + else: + if self.kw['use_filename_as_title']: + img_titles.append(fn) + else: + img_titles.append('') + self.logger.debug( + "Image {0} found in gallery but not listed in {1}". + format(fn, context['meta_path'])) + elif self.kw['use_filename_as_title']: img_titles = [] for fn in image_name_list: name_without_ext = os.path.splitext(os.path.basename(fn))[0] @@ -248,6 +266,7 @@ class Galleries(Task, ImageProcessor): folders = [] # Generate friendly gallery names + fpost_list = [] for path, folder in folder_list: fpost = self.parse_index(path, input_folder, output_folder) if fpost: @@ -256,8 +275,17 @@ class Galleries(Task, ImageProcessor): ft = folder if not folder.endswith('/'): folder += '/' - folders.append((folder, ft)) + # TODO: This is to keep compatibility with user's custom gallery.tmpl + # To be removed in v9 someday + if self.kw['galleries_use_thumbnail']: + folders.append((folder, ft, fpost)) + if fpost: + fpost_list.append(fpost.source_path) + else: + folders.append((folder, ft)) + + context["gallery_path"] = gallery context["folders"] = natsort.natsorted( folders, alg=natsort.ns.F | natsort.ns.IC) context["crumbs"] = utils.get_crumbs(gallery, index_folder=self, lang=lang) @@ -265,6 +293,7 @@ class Galleries(Task, ImageProcessor): context["enable_comments"] = self.kw['comments_in_galleries'] context["thumbnail_size"] = self.kw["thumbnail_size"] context["pagekind"] = ["gallery_front"] + context["galleries_use_thumbnail"] = self.kw['galleries_use_thumbnail'] if post: yield { @@ -291,7 +320,7 @@ class Galleries(Task, ImageProcessor): yield utils.apply_filters({ 'basename': self.name, 'name': dst, - 'file_dep': file_dep, + 'file_dep': file_dep + dest_img_list + fpost_list, 'targets': [dst], 'actions': [ (self.render_gallery_index, ( @@ -301,7 +330,7 @@ class Galleries(Task, ImageProcessor): dest_img_list, img_titles, thumbs, - file_dep))], + img_metadata))], 'clean': True, 'uptodate': [utils.config_changed({ 1: self.kw.copy(), @@ -343,7 +372,14 @@ class Galleries(Task, ImageProcessor): 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)) + # If output folder is empty, the top-level gallery + # index will collide with the main page for the site. + # Don't generate the top-level gallery index in that + # case. + # FIXME: also ignore pages named index + if (output_folder or root != input_folder and + (not self.kw['disable_indexes'] and self.kw['index_path'] == '')): + self.gallery_list.append((root, input_folder, output_folder)) def create_galleries_paths(self): """Given a list of galleries, put their paths into self.gallery_links.""" @@ -395,12 +431,73 @@ class Galleries(Task, ImageProcessor): 'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:mkdir')], } + def find_metadata(self, gallery, lang): + """Search for a gallery metadata file. + + If there is an metadata file for the gallery, use that to determine + captions and the order in which images shall be displayed in the + gallery. You only need to list the images if a specific ordering or + caption is required. The metadata file is YAML-formatted, with field + names of + # + name: + caption: + order: + # + If a numeric order value is specified, we use that directly, otherwise + we depend on how the library returns the information - which may or may not + be in the same order as in the file itself. Non-numeric ordering is not + supported. If no caption is specified, then we return an empty string. + Returns a string (l18n'd filename), list (ordering), dict (captions), + dict (image metadata). + """ + base_meta_path = os.path.join(gallery, "metadata.yml") + localized_meta_path = utils.get_translation_candidate(self.site.config, + base_meta_path, lang) + order = [] + captions = {} + custom_metadata = {} + used_path = "" + + if os.path.isfile(localized_meta_path): + used_path = localized_meta_path + elif os.path.isfile(base_meta_path): + used_path = base_meta_path + else: + return "", [], {}, {} + + self.logger.debug("Using {0} for gallery {1}".format( + used_path, gallery)) + with open(used_path, "r", encoding='utf-8-sig') as meta_file: + if YAML is None: + utils.req_missing(['ruamel.yaml'], 'use metadata.yml files for galleries') + yaml = YAML(typ='safe') + meta = yaml.load_all(meta_file) + for img in meta: + # load_all and safe_load_all both return None as their + # final element, so skip it + if not img: + continue + if 'name' in img: + img_name = img.pop('name') + if 'caption' in img and img['caption']: + captions[img_name] = img.pop('caption') + + if 'order' in img and img['order'] is not None: + order.insert(img.pop('order'), img_name) + else: + order.append(img_name) + custom_metadata[img_name] = img + else: + self.logger.error("no 'name:' for ({0}) in {1}".format( + img, used_path)) + return used_path, order, captions, custom_metadata + def parse_index(self, gallery, input_folder, output_folder): """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, - os.path.relpath(gallery, input_folder)) + destination = os.path.join(output_folder, + os.path.relpath(gallery, input_folder)) if os.path.isfile(index_path): post = Post( index_path, @@ -408,15 +505,18 @@ class Galleries(Task, ImageProcessor): destination, False, self.site.MESSAGES, - 'story.tmpl', - self.site.get_compiler(index_path) + 'page.tmpl', + self.site.get_compiler(index_path), + None, + self.site.metadata_extractors_by ) # If this did not exist, galleries without a title in the # index.txt file would be errorneously named `index` # (warning: galleries titled index and filenamed differently # may break) - if post.title == 'index': - post.title = os.path.split(gallery)[1] + if post.title() == 'index': + for lang in post.meta.keys(): + post.meta[lang]['title'] = os.path.split(gallery)[1] # Register the post (via #2417) self.site.post_per_input_file[index_path] = post else: @@ -428,8 +528,8 @@ class Galleries(Task, ImageProcessor): exclude_path = os.path.join(gallery_path, "exclude.meta") try: - f = open(exclude_path, 'r') - excluded_image_name_list = f.read().split() + with open(exclude_path, 'r') as f: + excluded_image_name_list = f.read().split() except IOError: excluded_image_name_list = [] @@ -473,34 +573,26 @@ class Galleries(Task, ImageProcessor): orig_dest_path = os.path.join(output_gallery, img_name) yield utils.apply_filters({ 'basename': self.name, - 'name': thumb_path, - 'file_dep': [img], - 'targets': [thumb_path], - 'actions': [ - (self.resize_image, - (img, thumb_path, self.kw['thumbnail_size'], False, self.kw['preserve_exif_data'], - self.kw['exif_whitelist'])) - ], - 'clean': True, - 'uptodate': [utils.config_changed({ - 1: self.kw['thumbnail_size'] - }, 'nikola.plugins.task.galleries:resize_thumb')], - }, self.kw['filters']) - - yield utils.apply_filters({ - 'basename': self.name, 'name': orig_dest_path, 'file_dep': [img], - 'targets': [orig_dest_path], + 'targets': [thumb_path, orig_dest_path], 'actions': [ (self.resize_image, - (img, orig_dest_path, self.kw['max_image_size'], False, self.kw['preserve_exif_data'], - self.kw['exif_whitelist'])) - ], + [img], { + 'dst_paths': [thumb_path, orig_dest_path], + 'max_sizes': [self.kw['thumbnail_size'], self.kw['max_image_size']], + 'bigger_panoramas': True, + 'preserve_exif_data': self.kw['preserve_exif_data'], + 'exif_whitelist': self.kw['exif_whitelist'], + 'preserve_icc_profiles': self.kw['preserve_icc_profiles']})], 'clean': True, 'uptodate': [utils.config_changed({ - 1: self.kw['max_image_size'] - }, 'nikola.plugins.task.galleries:resize_max')], + 1: self.kw['thumbnail_size'], + 2: self.kw['max_image_size'], + 3: self.kw['preserve_exif_data'], + 4: self.kw['exif_whitelist'], + 5: self.kw['preserve_icc_profiles'], + }, 'nikola.plugins.task.galleries:resize_thumb')], }, self.kw['filters']) def remove_excluded_image(self, img, input_folder): @@ -546,7 +638,7 @@ class Galleries(Task, ImageProcessor): img_list, img_titles, thumbs, - file_dep): + img_metadata): """Build the gallery index.""" # The photo array needs to be created here, because # it relies on thumbnails already being created on @@ -568,7 +660,7 @@ class Galleries(Task, ImageProcessor): else: img_list, thumbs, img_titles = [], [], [] - photo_array = [] + photo_info = OrderedDict() for img, thumb, title in zip(img_list, thumbs, img_titles): w, h = _image_size_cache.get(thumb, (None, None)) if w is None: @@ -578,8 +670,11 @@ class Galleries(Task, ImageProcessor): im = Image.open(thumb) w, h = im.size _image_size_cache[thumb] = w, h - # Thumbs are files in output, we need URLs - photo_array.append({ + im.close() + # Use basename to avoid issues with multilingual sites (Issue #3078) + img_basename = os.path.basename(img) + photo_info[img_basename] = { + # Thumbs are files in output, we need URLs 'url': url_from_path(img), 'url_thumb': url_from_path(thumb), 'title': title, @@ -587,9 +682,27 @@ class Galleries(Task, ImageProcessor): 'w': w, 'h': h }, - }) + 'width': w, + 'height': h + } + if img_basename in img_metadata: + photo_info[img_basename].update(img_metadata[img_basename]) + photo_array = [] + if context['order']: + for entry in context['order']: + photo_array.append(photo_info.pop(entry)) + # Do we have any orphan entries from metadata.yml, or + # are the files from the gallery not listed in metadata.yml? + if photo_info: + for entry in photo_info: + photo_array.append(photo_info[entry]) + else: + for entry in photo_info: + photo_array.append(photo_info[entry]) + context['photo_array'] = 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): @@ -647,6 +760,6 @@ class Galleries(Task, ImageProcessor): utils.makedirs(dst_dir) with io.open(output_path, "w+", encoding="utf-8") as rss_file: data = rss_obj.to_xml(encoding='utf-8') - if isinstance(data, utils.bytes_str): + if isinstance(data, bytes): data = data.decode('utf-8') rss_file.write(data) diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin index d3a34ee..cc078b7 100644 --- a/nikola/plugins/task/gzip.plugin +++ b/nikola/plugins/task/gzip.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create gzipped copies of files [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py index 79a11dc..ebd427f 100644 --- a/nikola/plugins/task/gzip.py +++ b/nikola/plugins/task/gzip.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/task/indexes.plugin b/nikola/plugins/task/indexes.plugin index 553b5ad..f4a8f05 100644 --- a/nikola/plugins/task/indexes.plugin +++ b/nikola/plugins/task/indexes.plugin @@ -1,5 +1,5 @@ [Core] -name = render_indexes +name = classify_indexes module = indexes [Documentation] @@ -9,5 +9,4 @@ website = https://getnikola.com/ description = Generates the blog's index pages. [Nikola] -plugincategory = Task - +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index 8ecd1de..20491fb 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,323 +24,114 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Render the blog indexes.""" +"""Render the blog's main index.""" -from __future__ import unicode_literals -from collections import defaultdict -import os -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA -from nikola.plugin_categories import Task -from nikola import utils -from nikola.nikola import _enclosure +from nikola.plugin_categories import Taxonomy -class Indexes(Task): - """Render the blog indexes.""" +class Indexes(Taxonomy): + """Classify for the blog's main index.""" - name = "render_indexes" + name = "classify_indexes" - def set_site(self, site): - """Set Nikola site.""" - self.number_of_pages = dict() - self.number_of_pages_section = {lang: dict() for lang in site.config['TRANSLATIONS']} - site.register_path_handler('index', self.index_path) - site.register_path_handler('index_atom', self.index_atom_path) - site.register_path_handler('section_index', self.index_section_path) - site.register_path_handler('section_index_atom', self.index_section_atom_path) - site.register_path_handler('section_index_rss', self.index_section_rss_path) - return super(Indexes, self).set_site(site) - - def _get_filtered_posts(self, lang, show_untranslated_posts): - """Return a filtered list of all posts for the given language. - - If show_untranslated_posts is True, will only include posts which - are translated to the given language. Otherwise, returns all posts. - """ - if show_untranslated_posts: - return self.site.posts - else: - return [x for x in self.site.posts if x.is_translation_available(lang)] - - def _compute_number_of_pages(self, filtered_posts, posts_count): - """Given a list of posts and the maximal number of posts per page, computes the number of pages needed.""" - return min(1, (len(filtered_posts) + posts_count - 1) // posts_count) - - def gen_tasks(self): - """Render the blog indexes.""" - self.site.scan_posts() - yield self.group_task() - - kw = { - "translations": self.site.config['TRANSLATIONS'], - "messages": self.site.MESSAGES, - "output_folder": self.site.config['OUTPUT_FOLDER'], - "feed_length": self.site.config['FEED_LENGTH'], - "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "filters": self.site.config['FILTERS'], - "index_file": self.site.config['INDEX_FILE'], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'], - "indexes_title": self.site.config['INDEXES_TITLE'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "blog_title": self.site.config["BLOG_TITLE"], - "generate_atom": self.site.config["GENERATE_ATOM"], - "site_url": self.site.config["SITE_URL"], - } - - template_name = "index.tmpl" - for lang in kw["translations"]: - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link("index" + feed, None, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - def page_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path("index" + feed, None, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - filtered_posts = self._get_filtered_posts(lang, kw["show_untranslated_posts"]) - - indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang) - self.number_of_pages[lang] = self._compute_number_of_pages(filtered_posts, kw['index_display_post_count']) - - context = {} - context["pagekind"] = ["main_index", "index"] - - yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path) - - if self.site.config['POSTS_SECTIONS']: - index_len = len(kw['index_file']) - - groups = defaultdict(list) - for p in filtered_posts: - groups[p.section_slug(lang)].append(p) - - # don't build sections when there is only one, aka. default setups - if not len(groups.items()) > 1: - continue - - for section_slug, post_list in groups.items(): - self.number_of_pages_section[lang][section_slug] = self._compute_number_of_pages(post_list, kw['index_display_post_count']) - - def cat_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link("section_index" + feed, section_slug, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - def cat_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path("section_index" + feed, section_slug, lang), i, displayed_i, - lang, self.site, force_addition, extension) + classification_name = "index" + overview_page_variable_name = None + more_than_one_classifications_per_post = False + has_hierarchy = False + show_list_as_index = True + template_for_single_list = "index.tmpl" + template_for_classification_overview = None + apply_to_posts = True + apply_to_pages = False + omit_empty_classifications = False + path_handler_docstrings = { + 'index_index': False, + 'index': """Link to a numbered index. - context = {} +Example: - short_destination = os.path.join(section_slug, kw['index_file']) - link = short_destination.replace('\\', '/') - if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']: - link = link[:-index_len] - context["permalink"] = link - context["pagekind"] = ["section_page"] - context["description"] = self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang)[section_slug] if section_slug in self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang) else "" +link://index/3 => /index-3.html""", + 'index_atom': """Link to a numbered Atom index. - if self.site.config["POSTS_SECTION_ARE_INDEXES"]: - context["pagekind"].append("index") - posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang) +Example: - section_title = None - if type(posts_section_title) is dict: - if section_slug in posts_section_title: - section_title = posts_section_title[section_slug] - elif type(posts_section_title) is str: - section_title = posts_section_title - if not section_title: - section_title = post_list[0].section_name(lang) - section_title = section_title.format(name=post_list[0].section_name(lang)) +link://index_atom/3 => /index-3.atom""", + 'index_rss': """A link to the RSS feed path. - task = self.site.generic_index_renderer(lang, post_list, section_title, "sectionindex.tmpl", context, kw, self.name, cat_link, cat_path) - else: - context["pagekind"].append("list") - output_name = os.path.join(kw['output_folder'], section_slug, kw['index_file']) - task = self.site.generic_post_list_renderer(lang, post_list, output_name, "list.tmpl", kw['filters'], context) - task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.indexes')] - task['basename'] = self.name - yield task +Example: - # RSS feed for section - deps = [] - deps_uptodate = [] - if kw["show_untranslated_posts"]: - posts = post_list[:kw['feed_length']] - else: - posts = [x for x in post_list if x.is_translation_available(lang)][:kw['feed_length']] - for post in posts: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) +link://rss => /blog/rss.xml""", + } - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link('section_index_rss', section_slug, lang).lstrip('/')) - output_name = os.path.join(kw['output_folder'], self.site.path('section_index_rss', section_slug, lang).lstrip(os.sep)) - task = { - 'basename': self.name, - 'name': os.path.normpath(output_name), - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"](lang), kw["site_url"], - context["description"], posts, output_name, - kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url, - _enclosure, kw["feed_links_append_query"]))], - - 'task_dep': ['render_posts'], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.indexes')] + deps_uptodate, - } - yield task - - if not self.site.config["PAGE_INDEX"]: - return + def set_site(self, site): + """Set Nikola site.""" + # Redirect automatically generated 'index_rss' path handler to 'rss' for compatibility with old rss plugin + site.register_path_handler('rss', lambda name, lang: site.path_handlers['index_rss'](name, lang)) + site.path_handlers['rss'].__doc__ = """A link to the RSS feed path. + +Example: + + link://rss => /blog/rss.xml + """.strip() + return super().set_site(site) + + def get_implicit_classifications(self, lang): + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [""] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + return [""] + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return self.site.config["BLOG_TITLE"](lang) + + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" + if dest_type == 'rss': + return [ + self.site.config['RSS_PATH'](lang), + self.site.config['RSS_FILENAME_BASE'](lang) + ], 'auto' + if dest_type == 'feed': + return [ + self.site.config['ATOM_PATH'](lang), + self.site.config['ATOM_FILENAME_BASE'](lang) + ], 'auto' + page_number = None + if dest_type == 'page': + # Interpret argument as page number + try: + page_number = int(classification) + except (ValueError, TypeError): + pass + return [self.site.config['INDEX_PATH'](lang)], 'always', page_number + + def provide_context_and_uptodate(self, classification, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" kw = { - "translations": self.site.config['TRANSLATIONS'], - "post_pages": self.site.config["post_pages"], - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - "index_file": self.site.config['INDEX_FILE'], - "strip_indexes": self.site.config['STRIP_INDEXES'], + "show_untranslated_posts": self.site.config["SHOW_UNTRANSLATED_POSTS"], } - 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: - 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 = {} - context["items"] = [] - should_render = True - 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('\\', '/') - 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 - # a page, do not generate the PAGE_INDEX - if post.destination_path(lang) == short_destination: - should_render = False - else: - context["items"].append((post.title(lang), - post.permalink(lang), - None)) - - if should_render: - task = self.site.generic_post_list_renderer(lang, post_list, - output_name, - template_name, - kw['filters'], - context) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')] - task['basename'] = self.name - yield task - - def index_path(self, name, lang, is_feed=False): - """Link to a numbered index. - - Example: - - link://index/3 => /index-3.html - """ - extension = None - if is_feed: - extension = ".atom" - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension - else: - index_file = self.site.config['INDEX_FILE'] - if lang in self.number_of_pages: - number_of_pages = self.number_of_pages[lang] - else: - number_of_pages = self._compute_number_of_pages(self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']), self.site.config['INDEX_DISPLAY_POST_COUNT']) - self.number_of_pages[lang] = number_of_pages - return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['INDEX_PATH'], - index_file] if _f], - name, - utils.get_displayed_page_number(name, number_of_pages, self.site), - lang, - self.site, - extension=extension) - - def index_section_path(self, name, lang, is_feed=False, is_rss=False): - """Link to the index for a section. - - Example: - - link://section_index/cars => /cars/index.html - """ - extension = None - - if is_feed: - extension = ".atom" - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension - elif is_rss: - index_file = 'rss.xml' - else: - index_file = self.site.config['INDEX_FILE'] - if name in self.number_of_pages_section[lang]: - number_of_pages = self.number_of_pages_section[lang][name] - else: - posts = [post for post in self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']) if post.section_slug(lang) == name] - number_of_pages = self._compute_number_of_pages(posts, self.site.config['INDEX_DISPLAY_POST_COUNT']) - self.number_of_pages_section[lang][name] = number_of_pages - return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang], - name, - index_file] if _f], - None, - utils.get_displayed_page_number(None, number_of_pages, self.site), - lang, - self.site, - extension=extension) - - def index_atom_path(self, name, lang): - """Link to a numbered Atom index. - - Example: - - link://index_atom/3 => /index-3.atom - """ - return self.index_path(name, lang, is_feed=True) - - def index_section_atom_path(self, name, lang): - """Link to the Atom index for a section. - - Example: - - link://section_index_atom/cars => /cars/index.atom - """ - return self.index_section_path(name, lang, is_feed=True) + context = { + "title": self.site.config["INDEXES_TITLE"](lang) or self.site.config["BLOG_TITLE"](lang), + "description": self.site.config["BLOG_DESCRIPTION"](lang), + "pagekind": ["main_index", "index"], + "featured": [p for p in self.site.posts if p.post_status == 'featured' and + (lang in p.translated_to or kw["show_untranslated_posts"])], + } + kw.update(context) + return context, kw - def index_section_rss_path(self, name, lang): - """Link to the RSS feed for a section. + def should_generate_classification_page(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + return not self.site.config["DISABLE_INDEXES"] - Example: + def should_generate_atom_for_classification_page(self, classification, post_list, lang): + """Only generates Atom feed for list of posts for classification if this function returns True.""" + return not self.site.config["DISABLE_MAIN_ATOM_FEED"] - link://section_index_rss/cars => /cars/rss.xml - """ - return self.index_section_path(name, lang, is_rss=True) + def should_generate_rss_for_classification_page(self, classification, post_list, lang): + """Only generates RSS feed for list of posts for classification if this function returns True.""" + return not self.site.config["DISABLE_MAIN_RSS_FEED"] diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin index 8fc2e2d..03b67d2 100644 --- a/nikola/plugins/task/listings.plugin +++ b/nikola/plugins/task/listings.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Render code listings into output [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py index e694aa5..c946313 100644 --- a/nikola/plugins/task/listings.py +++ b/nikola/plugins/task/listings.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,12 @@ """Render code listings.""" -from __future__ import unicode_literals, print_function - -from collections import defaultdict import os -import lxml.html +from collections import defaultdict +import natsort from pygments import highlight from pygments.lexers import get_lexer_for_filename, guess_lexer, TextLexer -import natsort from nikola.plugin_categories import Task from nikola import utils @@ -92,7 +89,7 @@ class Listings(Task): self.proper_input_file_mapping = {} for input_folder, output_folder in self.kw['listings_folders'].items(): - for root, dirs, files in os.walk(input_folder, followlinks=True): + for root, _, files in os.walk(input_folder, followlinks=True): # Compute relative path; can't use os.path.relpath() here as it returns "." instead of "" rel_path = root[len(input_folder):] if rel_path[:1] == os.sep: @@ -104,7 +101,7 @@ class Listings(Task): # Register file names in the mapping. self.register_output_name(input_folder, rel_name, rel_output_name) - return super(Listings, self).set_site(site) + return super().set_site(site) def gen_tasks(self): """Render pretty code listings.""" @@ -115,24 +112,31 @@ class Listings(Task): 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') + ipynb_plugin = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler") + if ipynb_plugin is None: + msg = "To use .ipynb files as listings, you must set up the Jupyter compiler in COMPILERS and POSTS/PAGES." + utils.LOGGER.error(msg) + raise ValueError(msg) + + ipynb_compiler = ipynb_plugin.plugin_object + with open(in_name, "r", encoding="utf-8-sig") as in_file: + nb_json = ipynb_compiler._nbformat_read(in_file) + code = ipynb_compiler._compile_string(nb_json) title = os.path.basename(in_name) needs_ipython_css = True elif in_name: - with open(in_name, 'r') as fd: + with open(in_name, 'r', encoding='utf-8-sig') as fd: try: lexer = get_lexer_for_filename(in_name) - except: + except Exception: try: lexer = guess_lexer(fd.read()) - except: + except Exception: lexer = TextLexer() fd.seek(0) - code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name)) + code = highlight( + fd.read(), lexer, + utils.NikolaPygmentsHTML(in_name, linenos='table')) title = os.path.basename(in_name) else: code = '' @@ -184,7 +188,7 @@ class Listings(Task): uptodate = {'c': self.site.GLOBAL_CONTEXT} for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): - uptodate['||template_hooks|{0}||'.format(k)] = v._items + uptodate['||template_hooks|{0}||'.format(k)] = v.calculate_deps() for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang']) @@ -220,6 +224,8 @@ class Listings(Task): 'clean': True, }, self.kw["filters"]) for f in files: + if f == '.DS_Store': + continue ext = os.path.splitext(f)[-1] if ext in ignored_extensions: continue @@ -257,7 +263,7 @@ class Listings(Task): }, self.kw["filters"]) def listing_source_path(self, name, lang): - """A link to the source code for a listing. + """Return a link to the source code for a listing. It will try to use the file name if it's not ambiguous, or the file path. @@ -273,7 +279,7 @@ class Listings(Task): return result def listing_path(self, namep, lang): - """A link to a listing. + """Return a link to a listing. It will try to use the file name if it's not ambiguous, or the file path. @@ -297,7 +303,7 @@ class Listings(Task): utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name]))) return ["ERROR"] if len(self.site.config['LISTINGS_FOLDERS']) > 1: - utils.LOGGER.notice("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.") + utils.LOGGER.warning("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.") name = list(self.improper_input_file_mapping[name])[0] break else: diff --git a/nikola/plugins/task/page_index.plugin b/nikola/plugins/task/page_index.plugin new file mode 100644 index 0000000..42c9288 --- /dev/null +++ b/nikola/plugins/task/page_index.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_page_index +module = page_index + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Generates the blog's index pages. + +[Nikola] +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/page_index.py b/nikola/plugins/task/page_index.py new file mode 100644 index 0000000..e7b33cf --- /dev/null +++ b/nikola/plugins/task/page_index.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the page index.""" + + +from nikola.plugin_categories import Taxonomy + + +class PageIndex(Taxonomy): + """Classify for the page index.""" + + name = "classify_page_index" + + classification_name = "page_index_folder" + overview_page_variable_name = "page_folder" + more_than_one_classifications_per_post = False + has_hierarchy = True + include_posts_from_subhierarchies = False + show_list_as_index = False + template_for_single_list = "list.tmpl" + template_for_classification_overview = None + always_disable_rss = True + always_disable_atom = True + apply_to_posts = False + apply_to_pages = True + omit_empty_classifications = True + path_handler_docstrings = { + 'page_index_folder_index': None, + 'page_index_folder': None, + 'page_index_folder_atom': None, + 'page_index_folder_rss': None, + } + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + return self.site.config["PAGE_INDEX"] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + destpath = post.destination_path(lang, sep='/') + if post.has_pretty_url(lang): + idx = '/index.html' + if destpath.endswith(idx): + destpath = destpath[:-len(idx)] + i = destpath.rfind('/') + return [destpath[:i] if i >= 0 else ''] + + def get_classification_friendly_name(self, dirname, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return dirname + + def get_path(self, hierarchy, lang, dest_type='page'): + """Return a path for the given classification.""" + return hierarchy, 'always' + + def extract_hierarchy(self, dirname): + """Given a classification, return a list of parts in the hierarchy.""" + return dirname.split('/') if dirname else [] + + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string.""" + return '/'.join(hierarchy) + + def provide_context_and_uptodate(self, dirname, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + kw = { + "translations": self.site.config['TRANSLATIONS'], + "filters": self.site.config['FILTERS'], + } + context = { + "title": self.site.config['BLOG_TITLE'](lang), + "pagekind": ["list", "front_page", "page_index"] if dirname == '' else ["list", "page_index"], + "kind": "page_index_folder", + "classification": dirname, + "has_no_feeds": True, + } + kw.update(context) + return context, kw + + def should_generate_classification_page(self, dirname, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + short_destination = dirname + '/' + self.site.config['INDEX_FILE'] + for post in post_list: + # If there is an index.html pending to be created from a page, do not generate the page index. + if post.destination_path(lang, sep='/') == short_destination: + return False + return True diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin index 1bdc7f4..a04cd05 100644 --- a/nikola/plugins/task/pages.plugin +++ b/nikola/plugins/task/pages.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create pages in the output. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py index 7d8287b..0c0bdd2 100644 --- a/nikola/plugins/task/pages.py +++ b/nikola/plugins/task/pages.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,9 +26,10 @@ """Render pages into output.""" -from __future__ import unicode_literals +import os + from nikola.plugin_categories import Task -from nikola.utils import config_changed +from nikola.utils import config_changed, LOGGER class RenderPages(Task): @@ -47,6 +48,13 @@ class RenderPages(Task): } self.site.scan_posts() yield self.group_task() + index_paths = {} + for lang in kw["translations"]: + index_paths[lang] = False + if not self.site.config["DISABLE_INDEXES"]: + index_paths[lang] = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'], + self.site.path('index', '', lang=lang))) + for lang in kw["translations"]: for post in self.site.timeline: if not kw["show_untranslated_posts"] and not post.is_translation_available(lang): @@ -56,6 +64,12 @@ class RenderPages(Task): else: context = {'pagekind': ['story_page', 'page_page']} for task in self.site.generic_page_renderer(lang, post, kw["filters"], context): + if task['name'] == index_paths[lang]: + # Issue 3022 + LOGGER.error( + "Post {0!r}: output path ({1}) conflicts with the blog index ({2}). " + "Please change INDEX_PATH or disable index generation.".format( + post.source_path, task['name'], index_paths[lang])) 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 c9578bc..6893472 100644 --- a/nikola/plugins/task/posts.plugin +++ b/nikola/plugins/task/posts.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create HTML fragments out of posts. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py index fe10c5f..5f48165 100644 --- a/nikola/plugins/task/posts.py +++ b/nikola/plugins/task/posts.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,11 +26,11 @@ """Build HTML fragments from metadata and text.""" -from copy import copy import os +from copy import copy from nikola.plugin_categories import Task -from nikola import filters, utils +from nikola import utils def update_deps(post, lang, task): @@ -85,11 +85,12 @@ class RenderPosts(Task): 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####")] + extra_targets = post.compiler.get_extra_targets(post, lang, dest) task = { 'basename': self.name, 'name': dest, 'file_dep': file_dep, - 'targets': [dest], + 'targets': [dest] + extra_targets, 'actions': [(post.compile, (lang, )), (update_deps, (post, lang, )), ], @@ -107,12 +108,9 @@ class RenderPosts(Task): for i, f in enumerate(ff): if not f: continue - if f.startswith('filters.'): # A function from the filters module - f = f[8:] - try: - flist.append(getattr(filters, f)) - except AttributeError: - pass + _f = self.site.filters.get(f) + if _f is not None: # A registered filter + flist.append(_f) else: flist.append(f) yield utils.apply_filters(task, {os.path.splitext(dest)[-1]: flist}) diff --git a/nikola/plugins/task/py3_switch.plugin b/nikola/plugins/task/py3_switch.plugin deleted file mode 100644 index b0014e1..0000000 --- a/nikola/plugins/task/py3_switch.plugin +++ /dev/null @@ -1,13 +0,0 @@ -[Core] -name = py3_switch -module = py3_switch - -[Documentation] -author = Roberto Alsina -version = 1.0 -website = https://getnikola.com/ -description = Beg the user to switch to Python 3 - -[Nikola] -plugincategory = Task - diff --git a/nikola/plugins/task/py3_switch.py b/nikola/plugins/task/py3_switch.py deleted file mode 100644 index 2ff4e2d..0000000 --- a/nikola/plugins/task/py3_switch.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2016 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Beg the user to switch to python 3.""" - -import datetime -import os -import random -import sys - -import doit.tools - -from nikola.utils import get_logger, STDERR_HANDLER -from nikola.plugin_categories import LateTask - -PY2_AND_NO_PY3_WARNING = """Nikola is going to deprecate Python 2 support in 2016. Your current -version will continue to work, but please consider upgrading to Python 3. - -Please check http://bit.ly/1FKEsiX for details. -""" -PY2_WARNING = """Nikola is going to deprecate Python 2 support in 2016. You already have Python 3 -available in your system. Why not switch? - -Please check http://bit.ly/1FKEsiX for details. -""" -PY2_BARBS = [ - "Python 2 has been deprecated for years. Stop clinging to your long gone youth and switch to Python3.", - "Python 2 is the safety blanket of languages. Be a big kid and switch to Python 3", - "Python 2 is old and busted. Python 3 is the new hotness.", - "Nice unicode you have there, would be a shame something happened to it.. switch to python 3!.", - "Don't get in the way of progress! Upgrade to Python 3 and save a developer's mind today!", - "Winners don't use Python 2 -- Signed: The FBI", - "Python 2? What year is it?", - "I just wanna tell you how I'm feeling\n" - "Gotta make you understand\n" - "Never gonna give you up [But Python 2 has to go]", - "The year 2009 called, and they want their Python 2.7 back.", -] - - -LOGGER = get_logger('Nikola', STDERR_HANDLER) - - -def has_python_3(): - """Check if python 3 is available.""" - if 'win' in sys.platform: - py_bin = 'py.exe' - else: - py_bin = 'python3' - for path in os.environ["PATH"].split(os.pathsep): - if os.access(os.path.join(path, py_bin), os.X_OK): - return True - return False - - -class Py3Switch(LateTask): - """Beg the user to switch to python 3.""" - - name = "_switch to py3" - - def gen_tasks(self): - """Beg the user to switch to python 3.""" - def give_warning(): - if sys.version_info[0] == 3: - return - if has_python_3(): - LOGGER.warn(random.choice(PY2_BARBS)) - LOGGER.warn(PY2_WARNING) - else: - LOGGER.warn(PY2_AND_NO_PY3_WARNING) - - task = { - 'basename': self.name, - 'name': 'please!', - 'actions': [give_warning], - 'clean': True, - 'uptodate': [doit.tools.timeout(datetime.timedelta(days=3))] - } - - return task diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin index c5a3042..57bd0c0 100644 --- a/nikola/plugins/task/redirect.plugin +++ b/nikola/plugins/task/redirect.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create redirect pages. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py index b170b81..a89fbd0 100644 --- a/nikola/plugins/task/redirect.py +++ b/nikola/plugins/task/redirect.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """Generate redirections.""" -from __future__ import unicode_literals import os @@ -45,12 +44,15 @@ class Redirect(Task): 'redirections': self.site.config['REDIRECTIONS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], 'filters': self.site.config['FILTERS'], + 'index_file': self.site.config['INDEX_FILE'], } yield self.group_task() if kw['redirections']: for src, dst in kw["redirections"]: src_path = os.path.join(kw["output_folder"], src.lstrip('/')) + if src_path.endswith("/"): + src_path += kw['index_file'] yield utils.apply_filters({ 'basename': self.name, 'name': src_path, diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin index 7ae56c6..51f7781 100644 --- a/nikola/plugins/task/robots.plugin +++ b/nikola/plugins/task/robots.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Generate /robots.txt exclusion file and promote sitemap. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py index 8537fc8..627d436 100644 --- a/nikola/plugins/task/robots.py +++ b/nikola/plugins/task/robots.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,13 +26,9 @@ """Generate a robots.txt file.""" -from __future__ import print_function, absolute_import, unicode_literals import io import os -try: - from urlparse import urljoin, urlparse -except ImportError: - from urllib.parse import urljoin, urlparse # NOQA +from urllib.parse import urljoin, urlparse from nikola.plugin_categories import LateTask from nikola import utils @@ -59,7 +55,8 @@ class RobotsFile(LateTask): def write_robots(): if kw["site_url"] != urljoin(kw["site_url"], "/"): - utils.LOGGER.warn('robots.txt not ending up in server root, will be useless') + utils.LOGGER.warning('robots.txt not ending up in server root, will be useless') + utils.LOGGER.info('Add "robots" to DISABLED_PLUGINS to disable this warning and robots.txt generation.') with io.open(robots_path, 'w+', encoding='utf8') as outf: outf.write("Sitemap: {0}\n\n".format(sitemapindex_url)) @@ -82,6 +79,6 @@ class RobotsFile(LateTask): "task_dep": ["sitemap"] }, kw["filters"]) elif kw["robots_exclusions"]: - utils.LOGGER.warn('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied file.') + utils.LOGGER.warning('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied file.') else: utils.LOGGER.debug('Did not generate robots.txt as one already exists in FILES_FOLDERS.') diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin deleted file mode 100644 index 4dd8aba..0000000 --- a/nikola/plugins/task/rss.plugin +++ /dev/null @@ -1,13 +0,0 @@ -[Core] -name = generate_rss -module = rss - -[Documentation] -author = Roberto Alsina -version = 1.0 -website = https://getnikola.com/ -description = Generate RSS feeds. - -[Nikola] -plugincategory = Task - diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py deleted file mode 100644 index 780559b..0000000 --- a/nikola/plugins/task/rss.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2012-2016 Roberto Alsina and others. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Generate RSS feeds.""" - -from __future__ import unicode_literals, print_function -import os -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA - -from nikola import utils -from nikola.nikola import _enclosure -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) - - def gen_tasks(self): - """Generate RSS feeds.""" - kw = { - "translations": self.site.config["TRANSLATIONS"], - "filters": self.site.config["FILTERS"], - "blog_title": self.site.config["BLOG_TITLE"], - "site_url": self.site.config["SITE_URL"], - "base_url": self.site.config["BASE_URL"], - "blog_description": self.site.config["BLOG_DESCRIPTION"], - "output_folder": self.site.config["OUTPUT_FOLDER"], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "feed_length": self.site.config['FEED_LENGTH'], - "feed_previewimage": self.site.config["FEED_PREVIEWIMAGE"], - "tzinfo": self.site.tzinfo, - "feed_read_more_link": self.site.config["FEED_READ_MORE_LINK"], - "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - } - self.site.scan_posts() - # Check for any changes in the state of use_in_feeds for any post. - # Issue #934 - kw['use_in_feeds_status'] = ''.join( - ['T' if x.use_in_feeds else 'F' for x in self.site.timeline] - ) - yield self.group_task() - for lang in kw["translations"]: - output_name = os.path.join(kw['output_folder'], - self.site.path("rss", None, lang)) - deps = [] - deps_uptodate = [] - if kw["show_untranslated_posts"]: - posts = self.site.posts[:kw['feed_length']] - else: - posts = [x for x in self.site.posts if x.is_translation_available(lang)][:kw['feed_length']] - for post in posts: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) - - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/')) - - task = { - 'basename': 'generate_rss', - 'name': os.path.normpath(output_name), - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"](lang), kw["site_url"], - kw["blog_description"](lang), posts, output_name, - kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url, - _enclosure, kw["feed_links_append_query"]))], - - 'task_dep': ['render_posts'], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.rss')] + deps_uptodate, - } - yield utils.apply_filters(task, kw['filters']) - - def rss_path(self, name, lang): - """A link to the RSS feed path. - - Example: - - link://rss => /blog/rss.xml - """ - 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 3edd0c6..332f583 100644 --- a/nikola/plugins/task/scale_images.plugin +++ b/nikola/plugins/task/scale_images.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Create down-scaled images and thumbnails. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py index 2b483ae..fa3a67b 100644 --- a/nikola/plugins/task/scale_images.py +++ b/nikola/plugins/task/scale_images.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2014-2016 Pelle Nilsson and others. +# Copyright © 2014-2020 Pelle Nilsson and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -38,29 +38,24 @@ class ScaleImage(Task, ImageProcessor): name = "scale_images" - def set_site(self, site): - """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): """Process all images in a src tree and put the (possibly) rescaled images in the dst folder.""" - ignore = set(['.svn']) + thumb_fmt = self.kw['image_thumbnail_format'] base_len = len(src.split(os.sep)) for root, dirs, files in os.walk(src, followlinks=True): root_parts = root.split(os.sep) - if set(root_parts) & ignore: - continue dst_dir = os.path.join(dst, *root_parts[base_len:]) utils.makedirs(dst_dir) for src_name in files: - if src_name in ('.DS_Store', 'Thumbs.db'): - continue if (not src_name.lower().endswith(tuple(self.image_ext_list)) and not src_name.upper().endswith(tuple(self.image_ext_list))): continue dst_file = os.path.join(dst_dir, src_name) src_file = os.path.join(root, src_name) - thumb_file = '.thumbnail'.join(os.path.splitext(dst_file)) + thumb_name, thumb_ext = os.path.splitext(src_name) + thumb_file = os.path.join(dst_dir, thumb_fmt.format( + name=thumb_name, + ext=thumb_ext, + )) yield { 'name': dst_file, 'file_dep': [src_file], @@ -71,19 +66,28 @@ 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, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist']) - self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist']) + self.resize_image( + src, + dst_paths=[dst, thumb], + max_sizes=[self.kw['max_image_size'], self.kw['image_thumbnail_size']], + bigger_panoramas=True, + preserve_exif_data=self.kw['preserve_exif_data'], + exif_whitelist=self.kw['exif_whitelist'], + preserve_icc_profiles=self.kw['preserve_icc_profiles'] + ) def gen_tasks(self): """Copy static files into the output folder.""" self.kw = { 'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'], + 'image_thumbnail_format': self.site.config['IMAGE_THUMBNAIL_FORMAT'], 'max_image_size': self.site.config['MAX_IMAGE_SIZE'], 'image_folders': self.site.config['IMAGE_FOLDERS'], 'output_folder': self.site.config['OUTPUT_FOLDER'], 'filters': self.site.config['FILTERS'], 'preserve_exif_data': self.site.config['PRESERVE_EXIF_DATA'], 'exif_whitelist': self.site.config['EXIF_WHITELIST'], + 'preserve_icc_profiles': self.site.config['PRESERVE_ICC_PROFILES'], } self.image_ext_list = self.image_ext_list_builtin diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin index 83e72c4..c8aa832 100644 --- a/nikola/plugins/task/sitemap.plugin +++ b/nikola/plugins/task/sitemap.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Generate google sitemap. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap.py index 64fcb45..8bbaa63 100644 --- a/nikola/plugins/task/sitemap/__init__.py +++ b/nikola/plugins/task/sitemap.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,18 +26,13 @@ """Generate a sitemap.""" -from __future__ import print_function, absolute_import, unicode_literals -import io import datetime -import dateutil.tz +import io import os -import sys -try: - from urlparse import urljoin, urlparse - import robotparser as robotparser -except ImportError: - from urllib.parse import urljoin, urlparse # NOQA - import urllib.robotparser as robotparser # NOQA +import urllib.robotparser as robotparser +from urllib.parse import urljoin, urlparse + +import dateutil.tz from nikola.plugin_categories import LateTask from nikola.utils import apply_filters, config_changed, encodelink @@ -119,7 +114,6 @@ class Sitemap(LateTask): "output_folder": self.site.config["OUTPUT_FOLDER"], "strip_indexes": self.site.config["STRIP_INDEXES"], "index_file": self.site.config["INDEX_FILE"], - "sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"], "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.php', '.xml', '.rss']), "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"], "filters": self.site.config["FILTERS"], @@ -142,18 +136,19 @@ class Sitemap(LateTask): 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']: + if not dirs and not files: continue # Totally empty, not on sitemap path = os.path.relpath(root, output) # ignore the current directory. if path == '.': - path = '' + path = syspath = '' else: + syspath = path + os.sep path = path.replace(os.sep, '/') + '/' lastmod = self.get_lastmod(root) loc = urljoin(base_url, base_path + path) if kw['index_file'] in files and kw['strip_indexes']: # ignore folders when not stripping urls - post = self.site.post_per_file.get(path + kw['index_file']) + post = self.site.post_per_file.get(syspath + kw['index_file']) if post and (post.is_draft or post.is_private or post.publish_later): continue alternates = [] @@ -169,7 +164,7 @@ class Sitemap(LateTask): continue # We already mapped the folder if os.path.splitext(fname)[-1] in mapped_exts: real_path = os.path.join(root, fname) - path = os.path.relpath(real_path, output) + path = syspath = os.path.relpath(real_path, output) if path.endswith(kw['index_file']) and kw['strip_indexes']: # ignore index files when stripping urls continue @@ -177,16 +172,15 @@ class Sitemap(LateTask): continue # read in binary mode to make ancient files work - fh = open(real_path, 'rb') - filehead = fh.read(1024) - fh.close() + with open(real_path, 'rb') as fh: + filehead = fh.read(1024) if path.endswith('.html') or path.endswith('.htm') or path.endswith('.php'): - """ ignores "html" files without doctype """ + # Ignores "html" files without doctype if b'<!doctype html' not in filehead.lower(): continue - """ ignores "html" files with noindex robot directives """ + # 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', @@ -207,7 +201,7 @@ class Sitemap(LateTask): continue else: continue # ignores all XML files except those presumed to be RSS - post = self.site.post_per_file.get(path) + post = self.site.post_per_file.get(syspath) if post and (post.is_draft or post.is_private or post.publish_later): continue path = path.replace(os.sep, '/') @@ -227,12 +221,8 @@ class Sitemap(LateTask): for rule in kw["robots_exclusions"]: robot = robotparser.RobotFileParser() robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)]) - if sys.version_info[0] == 3: - if not robot.can_fetch("*", '/' + path): - return False # not robot food - else: - if not robot.can_fetch("*", ('/' + path).encode('utf-8')): - return False # not robot food + if not robot.can_fetch("*", '/' + path): + return False # not robot food return True def write_sitemap(): @@ -322,6 +312,7 @@ class Sitemap(LateTask): 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 doctest.testmod() diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin index 66856f1..1ab1a3c 100644 --- a/nikola/plugins/task/sources.plugin +++ b/nikola/plugins/task/sources.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Copy page sources into the output. [Nikola] -plugincategory = Task +PluginCategory = Task diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py index 0d77aba..1d36429 100644 --- a/nikola/plugins/task/sources.py +++ b/nikola/plugins/task/sources.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -61,12 +61,8 @@ class Sources(Task): # do not publish PHP sources if post.source_ext(True) == post.compiler.extension(): continue - source = post.source_path - if lang != kw["default_lang"]: - source_lang = utils.get_translation_candidate(self.site.config, source, lang) - if os.path.exists(source_lang): - source = source_lang - if os.path.isfile(source): + source = post.translated_source_path(lang) + if source is not None and os.path.isfile(source): yield { 'basename': 'render_sources', 'name': os.path.normpath(output_name), diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin index c3a5be3..c17b7b3 100644 --- a/nikola/plugins/task/tags.plugin +++ b/nikola/plugins/task/tags.plugin @@ -1,5 +1,5 @@ [Core] -name = render_tags +name = classify_tags module = tags [Documentation] @@ -9,5 +9,4 @@ website = https://getnikola.com/ description = Render the tag pages and feeds. [Nikola] -plugincategory = Task - +PluginCategory = Taxonomy diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py index 8b4683e..aecf8f5 100644 --- a/nikola/plugins/task/tags.py +++ b/nikola/plugins/task/tags.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,497 +24,137 @@ # 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.""" +"""Render the tag pages and feeds.""" -from __future__ import unicode_literals -import json -import os -import natsort -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA -from nikola.plugin_categories import Task +from nikola.plugin_categories import Taxonomy from nikola import utils -from nikola.nikola import _enclosure -class RenderTags(Task): - """Render the tag/category pages and feeds.""" +class ClassifyTags(Taxonomy): + """Classify the posts by tags.""" - name = "render_tags" + name = "classify_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) - site.register_path_handler('tag_atom', self.tag_atom_path) - site.register_path_handler('tag_rss', self.tag_rss_path) - site.register_path_handler('category', self.category_path) - site.register_path_handler('category_atom', self.category_atom_path) - site.register_path_handler('category_rss', self.category_rss_path) - return super(RenderTags, self).set_site(site) - - def gen_tasks(self): - """Render the tag pages and feeds.""" - kw = { - "translations": self.site.config["TRANSLATIONS"], - "blog_title": self.site.config["BLOG_TITLE"], - "site_url": self.site.config["SITE_URL"], - "base_url": self.site.config["BASE_URL"], - "messages": self.site.MESSAGES, - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - 'tag_path': self.site.config['TAG_PATH'], - "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'], - 'category_path': self.site.config['CATEGORY_PATH'], - 'category_prefix': self.site.config['CATEGORY_PREFIX'], - "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'], - "generate_rss": self.site.config['GENERATE_RSS'], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "feed_length": self.site.config['FEED_LENGTH'], - "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'], - "tzinfo": self.site.tzinfo, - "pretty_urls": self.site.config['PRETTY_URLS'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "index_file": self.site.config['INDEX_FILE'], - "category_pages_descriptions": self.site.config['CATEGORY_PAGES_DESCRIPTIONS'], - "category_pages_titles": self.site.config['CATEGORY_PAGES_TITLES'], - "tag_pages_descriptions": self.site.config['TAG_PAGES_DESCRIPTIONS'], - "tag_pages_titles": self.site.config['TAG_PAGES_TITLES'], - } - - self.site.scan_posts() - yield self.group_task() - - yield self.list_tags_page(kw) - - if not self.site.posts_per_tag and not self.site.posts_per_category: - return - - for lang in kw["translations"]: - if kw['category_path'][lang] == kw['tag_path'][lang]: - tags = {self.slugify_tag_name(tag, lang): tag for tag in self.site.tags_per_language[lang]} - cats = {tuple(self.slugify_category_name(category, lang)): category for category in self.site.posts_per_category.keys()} - categories = {k[0]: v for k, v in cats.items() if len(k) == 1} - intersect = set(tags.keys()) & set(categories.keys()) - if len(intersect) > 0: - for slug in intersect: - utils.LOGGER.error("Category '{0}' and tag '{1}' both have the same slug '{2}' for language {3}!".format('/'.join(categories[slug]), tags[slug], slug, lang)) - - # Test for category slug clashes - categories = {} - for category in self.site.posts_per_category.keys(): - slug = tuple(self.slugify_category_name(category, lang)) - for part in slug: - if len(part) == 0: - utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug))) - raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug))) - if slug in categories: - other_category = categories[slug] - utils.LOGGER.error('You have categories that are too similar: {0} and {1} (language {2})'.format(category, other_category, lang)) - utils.LOGGER.error('Category {0} is used in: {1}'.format(category, ', '.join([p.source_path for p in self.site.posts_per_category[category]]))) - utils.LOGGER.error('Category {0} is used in: {1}'.format(other_category, ', '.join([p.source_path for p in self.site.posts_per_category[other_category]]))) - raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug))) - categories[slug] = category - - tag_list = list(self.site.posts_per_tag.items()) - cat_list = list(self.site.posts_per_category.items()) - - def render_lists(tag, posts, is_category=True): - """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"]: - if kw["show_untranslated_posts"]: - filtered_posts = post_list - else: - filtered_posts = [x for x in post_list if x.is_translation_available(lang)] - if kw["generate_rss"]: - yield self.tag_rss(tag, lang, filtered_posts, kw, is_category) - # Render HTML - if kw['category_pages_are_indexes'] if is_category else kw['tag_pages_are_indexes']: - yield self.tag_page_as_index(tag, lang, filtered_posts, kw, is_category) - else: - yield self.tag_page_as_list(tag, lang, filtered_posts, kw, is_category) - - for tag, posts in tag_list: - for task in render_lists(tag, posts, False): - yield task - - for path, posts in cat_list: - for task in render_lists(path, posts, True): - yield task - - # Tag cloud json file - tag_cloud_data = {} - for tag, posts in self.site.posts_per_tag.items(): - if tag in self.site.config['HIDDEN_TAGS']: - continue - tag_posts = dict(posts=[{'title': post.meta[post.default_lang]['title'], - 'date': post.date.strftime('%m/%d/%Y'), - 'isodate': post.date.isoformat(), - 'url': post.permalink(post.default_lang)} - for post in reversed(sorted(self.site.timeline, key=lambda post: post.date)) - if tag in post.alltags]) - tag_cloud_data[tag] = [len(posts), self.site.link( - 'tag', tag, self.site.config['DEFAULT_LANG']), tag_posts] - output_name = os.path.join(kw['output_folder'], - 'assets', 'js', 'tag_cloud_data.json') + classification_name = "tag" + overview_page_variable_name = "tags" + overview_page_items_variable_name = "items" + more_than_one_classifications_per_post = True + has_hierarchy = False + show_list_as_subcategories_list = False + template_for_classification_overview = "tags.tmpl" + always_disable_rss = False + always_disable_atom = False + apply_to_posts = True + apply_to_pages = False + omit_empty_classifications = True + add_other_languages_variable = True + path_handler_docstrings = { + 'tag_index': """A link to the tag index. - 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, sort_keys=True) +Example: - if self.site.config['WRITE_TAG_CLOUD']: - task = { - 'basename': str(self.name), - 'name': str(output_name) - } +link://tag_index => /tags/index.html""", + 'tag': """A link to a tag's page. Takes page number as optional keyword argument. - task['uptodate'] = [utils.config_changed(tag_cloud_data, 'nikola.plugins.task.tags:tagdata')] - task['targets'] = [output_name] - task['actions'] = [(write_tag_data, [tag_cloud_data])] - task['clean'] = True - yield utils.apply_filters(task, kw['filters']) +Example: - def _create_tags_page(self, kw, lang, include_tags=True, include_categories=True): - """Create a global "all your tags/categories" page for each language.""" - categories = [cat.category_name for cat in self.site.category_hierarchy] - has_categories = (categories != []) and include_categories - template_name = "tags.tmpl" - kw = kw.copy() - if include_categories: - kw['categories'] = categories - 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)) - context = {} - if has_categories and has_tags: - context["title"] = kw["messages"][lang]["Tags and Categories"] - elif has_categories: - context["title"] = kw["messages"][lang]["Categories"] - else: - context["title"] = kw["messages"][lang]["Tags"] - if has_tags: - context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag - in tags] - else: - context["items"] = None - if has_categories: - context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag - in categories] - context['cat_hierarchy'] = [(node.name, node.category_name, node.category_path, - self.site.link("category", node.category_name), - node.indent_levels, node.indent_change_before, - node.indent_change_after) - for node in self.site.category_hierarchy] - else: - 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, - [], - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:page')] - task['basename'] = str(self.name) - yield task - - def list_tags_page(self, kw): - """Create a global "all your tags/categories" page for each language.""" - for lang in kw["translations"]: - if self.site.config['TAG_PATH'][lang] == self.site.config['CATEGORY_PATH'][lang]: - yield self._create_tags_page(kw, lang, True, True) - else: - yield self._create_tags_page(kw, lang, False, True) - yield self._create_tags_page(kw, lang, True, False) - - def _get_title(self, tag, is_category): - if is_category: - return self.site.parse_category_name(tag)[-1] - else: - return tag - - def _get_indexes_title(self, tag, nice_tag, is_category, lang, messages): - titles = self.site.config['CATEGORY_PAGES_TITLES'] if is_category else self.site.config['TAG_PAGES_TITLES'] - return titles[lang][tag] if lang in titles and tag in titles[lang] else messages[lang]["Posts about %s"] % nice_tag - - def _get_description(self, tag, is_category, lang): - descriptions = self.site.config['CATEGORY_PAGES_DESCRIPTIONS'] if is_category else self.site.config['TAG_PAGES_DESCRIPTIONS'] - return descriptions[lang][tag] if lang in descriptions and tag in descriptions[lang] else None - - def _get_subcategories(self, category): - node = self.site.category_hierarchy_lookup[category] - return [(child.name, self.site.link("category", child.category_name)) for child in node.children] +link://tag/cats => /tags/cats.html""", + 'tag_atom': """A link to a tag's Atom feed. - def tag_page_as_index(self, tag, lang, post_list, kw, is_category): - """Render a sort of index page collection using only this tag's posts.""" - kind = "category" if is_category else "tag" +Example: - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension) +link://tag_atom/cats => /tags/cats.atom""", + 'tag_rss': """A link to a tag's RSS feed. - def page_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension) +Example: - context_source = {} - title = self._get_title(tag, is_category) - if kw["generate_rss"]: - # On a tag page, the feeds include the tag's feeds - rss_link = ("""<link rel="alternate" type="application/rss+xml" """ - """title="RSS for tag """ - """{0} ({1})" href="{2}">""".format( - title, lang, self.site.link(kind + "_rss", tag, lang))) - context_source['rss_link'] = rss_link - if is_category: - context_source["category"] = tag - context_source["category_path"] = self.site.parse_category_name(tag) - context_source["tag"] = title - indexes_title = self._get_indexes_title(tag, title, is_category, lang, kw["messages"]) - 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" +link://tag_rss/cats => /tags/cats.xml""", + } - 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): - """Render a single flat link list with this tag's posts.""" - kind = "category" if is_category else "tag" - template_name = "tag.tmpl" - output_name = os.path.join(kw['output_folder'], self.site.path( - kind, tag, lang)) - context = {} - context["lang"] = lang - title = self._get_title(tag, is_category) - if is_category: - context["category"] = tag - context["category_path"] = self.site.parse_category_name(tag) - context["tag"] = title - context["title"] = self._get_indexes_title(tag, title, is_category, lang, kw["messages"]) - context["posts"] = post_list - context["permalink"] = self.site.link(kind, tag, lang) - context["kind"] = kind - 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, - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')] - task['basename'] = str(self.name) - yield task - - if self.site.config['GENERATE_ATOM']: - yield self.atom_feed_list(kind, tag, lang, post_list, context, kw) + def set_site(self, site): + """Set site, which is a Nikola instance.""" + super().set_site(site) + self.show_list_as_index = self.site.config['TAG_PAGES_ARE_INDEXES'] + self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl" + self.minimum_post_count_per_classification_in_overview = self.site.config['TAGLIST_MINIMUM_POSTS'] + self.translation_manager = utils.ClassificationTranslationManager() - def atom_feed_list(self, kind, tag, lang, post_list, context, kw): - """Generate atom feeds for tag lists.""" - if kind == 'tag': - context['feedlink'] = self.site.abs_link(self.site.path('tag_atom', tag, lang)) - feed_path = os.path.join(kw['output_folder'], self.site.path('tag_atom', tag, lang)) - elif kind == 'category': - context['feedlink'] = self.site.abs_link(self.site.path('category_atom', tag, lang)) - feed_path = os.path.join(kw['output_folder'], self.site.path('category_atom', tag, lang)) + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + return True - task = { - 'basename': str(self.name), - 'name': feed_path, - 'targets': [feed_path], - 'actions': [(self.site.atom_feed_renderer, (lang, post_list, feed_path, kw['filters'], context))], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:atom')], - 'task_dep': ['render_posts'], - } - return task + def classify(self, post, lang): + """Classify the given post for the given language.""" + return post.tags_for_language(lang) - def tag_rss(self, tag, lang, posts, kw, is_category): - """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( - os.path.join(kw['output_folder'], - self.site.path(kind + "_rss", tag, lang))) - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/')) - deps = [] - deps_uptodate = [] - post_list = sorted(posts, key=lambda a: a.date) - post_list.reverse() - for post in post_list: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) - task = { - 'basename': str(self.name), - 'name': output_name, - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, is_category)), - kw["site_url"], None, post_list, - output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], - feed_url, _enclosure, kw["feed_link_append_query"]))], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate, - 'task_dep': ['render_posts'], - } - return utils.apply_filters(task, kw['filters']) + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return classification def slugify_tag_name(self, name, lang): """Slugify a tag name.""" - if lang is None: # TODO: remove in v8 - utils.LOGGER.warn("RenderTags.slugify_tag_name() called without language!") - lang = '' if self.site.config['SLUG_TAG_PATH']: name = utils.slugify(name, lang) return name - def tag_index_path(self, name, lang): - """A link to the tag index. - - Example: - - link://tag_index => /tags/index.html - """ - if self.site.config['TAGS_INDEX_PATH'][lang]: - paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['TAGS_INDEX_PATH'][lang]] if _f] + def get_overview_path(self, lang, dest_type='page'): + """Return a path for the list of all classifications.""" + if self.site.config['TAGS_INDEX_PATH'](lang): + path = self.site.config['TAGS_INDEX_PATH'](lang) + append_index = 'never' else: - paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'][lang], - self.site.config['INDEX_FILE']] if _f] - return paths - - def category_index_path(self, name, lang): - """A link to the category index. - - Example: - - link://category_index => /categories/index.html - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['CATEGORY_PATH'][lang], - self.site.config['INDEX_FILE']] if _f] - - def tag_path(self, name, lang): - """A link to a tag's page. - - Example: - - link://tag/cats => /tags/cats.html - """ - if self.site.config['PRETTY_URLS']: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'][lang], - self.slugify_tag_name(name, lang), - self.site.config['INDEX_FILE']] if _f] - else: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'][lang], - self.slugify_tag_name(name, lang) + ".html"] if _f] - - def tag_atom_path(self, name, lang): - """A link to a tag's Atom feed. - - Example: - - link://tag_atom/cats => /tags/cats.atom - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".atom"] if - _f] - - def tag_rss_path(self, name, lang): - """A link to a tag's RSS feed. - - Example: - - link://tag_rss/cats => /tags/cats.xml - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".xml"] if - _f] - - def slugify_category_name(self, name, lang): - """Slugify a category name.""" - if lang is None: # TODO: remove in v8 - utils.LOGGER.warn("RenderTags.slugify_category_name() called without language!") - lang = '' - path = self.site.parse_category_name(name) - if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']: - path = path[-1:] # only the leaf - result = [self.slugify_tag_name(part, lang) for part in path] - result[0] = self.site.config['CATEGORY_PREFIX'] + result[0] - if not self.site.config['PRETTY_URLS']: - result = ['-'.join(result)] - return result - - def _add_extension(self, path, extension): - path[-1] += extension - return path - - def category_path(self, name, lang): - """A link to a category. - - Example: - - link://category/dogs => /categories/dogs.html - """ - if self.site.config['PRETTY_URLS']: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['CATEGORY_PATH'][lang]] if - _f] + self.slugify_category_name(name, lang) + [self.site.config['INDEX_FILE']] - else: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['CATEGORY_PATH'][lang]] if - _f] + self._add_extension(self.slugify_category_name(name, lang), ".html") - - def category_atom_path(self, name, lang): - """A link to a category's Atom feed. - - Example: - - link://category_atom/dogs => /categories/dogs.atom - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['CATEGORY_PATH'][lang]] if - _f] + self._add_extension(self.slugify_category_name(name, lang), ".atom") + path = self.site.config['TAG_PATH'](lang) + append_index = 'always' + return [component for component in path.split('/') if component], append_index + + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" + return [_f for _f in [ + self.site.config['TAG_PATH'](lang), + self.slugify_tag_name(classification, lang)] if _f], 'auto' + + def provide_overview_context_and_uptodate(self, lang): + """Provide data for the context and the uptodate list for the list of all classifiations.""" + kw = { + "tag_path": self.site.config['TAG_PATH'], + "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'], + "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'], + "tzinfo": self.site.tzinfo, + "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'], + "tag_titles": self.site.config['TAG_TITLES'], + } + context = { + "title": self.site.MESSAGES[lang]["Tags"], + "description": self.site.MESSAGES[lang]["Tags"], + "pagekind": ["list", "tags_page"], + } + kw.update(context) + return context, kw - def category_rss_path(self, name, lang): - """A link to a category's RSS feed. + def provide_context_and_uptodate(self, classification, lang, node=None): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + kw = { + "tag_path": self.site.config['TAG_PATH'], + "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'], + "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'], + "tzinfo": self.site.tzinfo, + "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'], + "tag_titles": self.site.config['TAG_TITLES'], + } + context = { + "title": self.site.config['TAG_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % classification), + "description": self.site.config['TAG_DESCRIPTIONS'].get(lang, {}).get(classification), + "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"], + "tag": classification, + } + kw.update(context) + return context, kw - Example: + def get_other_language_variants(self, classification, lang, classifications_per_language): + """Return a list of variants of the same tag in other languages.""" + return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language) - link://category_rss/dogs => /categories/dogs.xml - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['CATEGORY_PATH'][lang]] if - _f] + self._add_extension(self.slugify_category_name(name, lang), ".xml") + def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + self.translation_manager.read_from_config(self.site, 'TAG', posts_per_classification_per_language, False) diff --git a/nikola/plugins/task/taxonomies.plugin b/nikola/plugins/task/taxonomies.plugin new file mode 100644 index 0000000..5bda812 --- /dev/null +++ b/nikola/plugins/task/taxonomies.plugin @@ -0,0 +1,12 @@ +[Core] +name = render_taxonomies +module = taxonomies + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Render the taxonomy overviews, classification pages and feeds. + +[Nikola] +PluginCategory = Task diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py new file mode 100644 index 0000000..7dcf6ed --- /dev/null +++ b/nikola/plugins/task/taxonomies.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2020 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the taxonomy overviews, classification pages and feeds.""" + +import os +from collections import defaultdict +from copy import copy +from urllib.parse import urljoin + +import blinker +import natsort + +from nikola import utils, hierarchy_utils +from nikola.nikola import _enclosure +from nikola.plugin_categories import Task + + +class RenderTaxonomies(Task): + """Render taxonomy pages and feeds.""" + + name = "render_taxonomies" + + def _generate_classification_overview_kw_context(self, taxonomy, lang): + """Create context and kw for a classification overview page.""" + context, kw = taxonomy.provide_overview_context_and_uptodate(lang) + + context = copy(context) + context["kind"] = "{}_index".format(taxonomy.classification_name) + sorted_links = [] + for other_lang in sorted(self.site.config['TRANSLATIONS'].keys()): + if other_lang != lang: + sorted_links.append((other_lang, None, None)) + # Put the current language in front, so that it appears first in links + # (Issue #3248) + sorted_links_all = [(lang, None, None)] + sorted_links + context['has_other_languages'] = True + context['other_languages'] = sorted_links + context['all_languages'] = sorted_links_all + + kw = copy(kw) + kw["messages"] = self.site.MESSAGES + kw["translations"] = self.site.config['TRANSLATIONS'] + kw["filters"] = self.site.config['FILTERS'] + kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview + kw["output_folder"] = self.site.config['OUTPUT_FOLDER'] + kw["pretty_urls"] = self.site.config['PRETTY_URLS'] + kw["strip_indexes"] = self.site.config['STRIP_INDEXES'] + kw["index_file"] = self.site.config['INDEX_FILE'] + + # Collect all relevant classifications + if taxonomy.has_hierarchy: + def acceptor(node): + return len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang)) >= kw["minimum_post_count"] + + clipped_root_list = [hierarchy_utils.clone_treenode(node, parent=None, acceptor=acceptor) for node in self.site.hierarchy_per_classification[taxonomy.classification_name][lang]] + clipped_root_list = [node for node in clipped_root_list if node] + clipped_flat_hierarchy = hierarchy_utils.flatten_tree_structure(clipped_root_list) + + classifications = [cat.classification_name for cat in clipped_flat_hierarchy] + else: + classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items() + if len(self._filter_list(posts, lang)) >= kw["minimum_post_count"]], + alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications, lang) + + # Set up classifications in context + context[taxonomy.overview_page_variable_name] = classifications + context["has_hierarchy"] = taxonomy.has_hierarchy + if taxonomy.overview_page_items_variable_name: + items = [(classification, + self.site.link(taxonomy.classification_name, classification, lang)) + for classification in classifications] + items_with_postcount = [ + (classification, + self.site.link(taxonomy.classification_name, classification, lang), + len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][classification], lang))) + for classification in classifications + ] + context[taxonomy.overview_page_items_variable_name] = items + context[taxonomy.overview_page_items_variable_name + "_with_postcount"] = items_with_postcount + if taxonomy.has_hierarchy and taxonomy.overview_page_hierarchy_variable_name: + hier_items = [ + (node.name, node.classification_name, node.classification_path, + self.site.link(taxonomy.classification_name, node.classification_name, lang), + node.indent_levels, node.indent_change_before, + node.indent_change_after) + for node in clipped_flat_hierarchy + ] + hier_items_with_postcount = [ + (node.name, node.classification_name, node.classification_path, + self.site.link(taxonomy.classification_name, node.classification_name, lang), + node.indent_levels, node.indent_change_before, + node.indent_change_after, + len(node.children), + len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang))) + for node in clipped_flat_hierarchy + ] + context[taxonomy.overview_page_hierarchy_variable_name] = hier_items + context[taxonomy.overview_page_hierarchy_variable_name + '_with_postcount'] = hier_items_with_postcount + return context, kw + + def _render_classification_overview(self, classification_name, template, lang, context, kw): + # Prepare rendering + context["permalink"] = self.site.link("{}_index".format(classification_name), None, lang) + if "pagekind" not in context: + context["pagekind"] = ["list", "tags_page"] + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(classification_name), None, lang)) + blinker.signal('generate_classification_overview').send({ + 'site': self.site, + 'classification_name': classification_name, + 'lang': lang, + 'context': context, + 'kw': kw, + 'output_name': output_name, + }) + task = self.site.generic_post_list_renderer( + lang, + [], + output_name, + template, + kw['filters'], + context, + ) + task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:page')] + task['basename'] = str(self.name) + yield task + + def _generate_classification_overview(self, taxonomy, lang): + """Create a global "all your tags/categories" page for a given language.""" + context, kw = self._generate_classification_overview_kw_context(taxonomy, lang) + for task in self._render_classification_overview(taxonomy.classification_name, taxonomy.template_for_classification_overview, lang, context, kw): + yield task + + def _generate_tag_and_category_overview(self, tag_taxonomy, category_taxonomy, lang): + """Create a global "all your tags/categories" page for a given language.""" + # Create individual contexts and kw dicts + tag_context, tag_kw = self._generate_classification_overview_kw_context(tag_taxonomy, lang) + cat_context, cat_kw = self._generate_classification_overview_kw_context(category_taxonomy, lang) + + # Combine resp. select dicts + if tag_context['items'] and cat_context['cat_items']: + # Combine contexts. We must merge the tag context into the category context + # so that tag_context['items'] makes it into the result. + context = cat_context + context.update(tag_context) + kw = cat_kw + kw.update(tag_kw) + + # Update title + title = self.site.MESSAGES[lang]["Tags and Categories"] + context['title'] = title + context['description'] = title + kw['title'] = title + kw['description'] = title + elif cat_context['cat_items']: + # Use category overview page + context = cat_context + kw = cat_kw + else: + # Use tag overview page + context = tag_context + kw = tag_kw + + # Render result + for task in self._render_classification_overview('tag', tag_taxonomy.template_for_classification_overview, lang, context, kw): + yield task + + def _generate_classification_page_as_rss(self, taxonomy, classification, filtered_posts, title, description, kw, lang): + """Create a RSS feed for a single classification in a given language.""" + kind = taxonomy.classification_name + # Render RSS + output_name = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind + "_rss", classification, lang))) + feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", classification, lang).lstrip('/')) + deps = [] + deps_uptodate = [] + for post in filtered_posts: + deps += post.deps(lang) + deps_uptodate += post.deps_uptodate(lang) + blog_title = kw["blog_title"](lang) + task = { + 'basename': str(self.name), + 'name': output_name, + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, "{0} ({1})".format(blog_title, title) if blog_title != title else blog_title, + kw["site_url"], description, filtered_posts, + output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], + feed_url, _enclosure, kw["feed_links_append_query"]))], + 'clean': True, + 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:rss')] + deps_uptodate, + 'task_dep': ['render_posts'], + } + return utils.apply_filters(task, kw['filters']) + + def _generate_classification_page_as_index(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Render an index page collection using only this classification's posts.""" + kind = taxonomy.classification_name + + def page_link(i, displayed_i, num_pages, force_addition, extension=None): + return self.site.link(kind, classification, lang, alternative_path=force_addition, page=i) + + def page_path(i, displayed_i, num_pages, force_addition, extension=None): + return self.site.path(kind, classification, lang, alternative_path=force_addition, page=i) + + context = copy(context) + context["kind"] = kind + if "pagekind" not in context: + context["pagekind"] = ["index", "tag_page"] + template_name = taxonomy.template_for_single_list + + yield self.site.generic_index_renderer(lang, filtered_posts, context['title'], template_name, context, kw, str(self.name), page_link, page_path) + + def _generate_classification_page_as_atom(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Generate atom feeds for classification lists.""" + kind = taxonomy.classification_name + + context = copy(context) + context["kind"] = kind + + yield self.site.generic_atom_renderer(lang, filtered_posts, context, kw, str(self.name), classification, kind) + + def _generate_classification_page_as_list(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Render a single flat link list with this classification's posts.""" + kind = taxonomy.classification_name + template_name = taxonomy.template_for_single_list + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang)) + context["lang"] = lang + # list.tmpl expects a different format than list_post.tmpl (Issue #2701) + if template_name == 'list.tmpl': + context["items"] = [(post.title(lang), post.permalink(lang), None) for post in filtered_posts] + else: + context["posts"] = filtered_posts + if "pagekind" not in context: + context["pagekind"] = ["list", "tag_page"] + task = self.site.generic_post_list_renderer(lang, filtered_posts, output_name, template_name, kw['filters'], context) + task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:list')] + task['basename'] = str(self.name) + yield task + + def _filter_list(self, post_list, lang): + """Return only the posts which should be shown for this language.""" + if self.site.config["SHOW_UNTRANSLATED_POSTS"]: + return post_list + else: + return [x for x in post_list if x.is_translation_available(lang)] + + def _generate_subclassification_page(self, taxonomy, node, context, kw, lang): + """Render a list of subclassifications.""" + def get_subnode_data(subnode): + return [ + taxonomy.get_classification_friendly_name(subnode.classification_name, lang, only_last_component=True), + self.site.link(taxonomy.classification_name, subnode.classification_name, lang), + len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][subnode.classification_name], lang)) + ] + + items = [get_subnode_data(subnode) for subnode in node.children] + context = copy(context) + context["lang"] = lang + context["permalink"] = self.site.link(taxonomy.classification_name, node.classification_name, lang) + if "pagekind" not in context: + context["pagekind"] = ["list", "archive_page"] + context["items"] = items + task = self.site.generic_post_list_renderer( + lang, + [], + os.path.join(kw['output_folder'], self.site.path(taxonomy.classification_name, node.classification_name, lang)), + taxonomy.subcategories_list_template, + kw['filters'], + context, + ) + task_cfg = {1: kw, 2: items} + task['uptodate'] = task['uptodate'] + [utils.config_changed(task_cfg, 'nikola.plugins.task.taxonomy')] + task['basename'] = self.name + return task + + def _generate_classification_page(self, taxonomy, classification, filtered_posts, generate_list, generate_rss, generate_atom, lang, post_lists_per_lang, classification_set_per_lang=None): + """Render index or post list and associated feeds per classification.""" + # Should we create this list? + if not any((generate_list, generate_rss, generate_atom)): + return + # Get data + node = None + if taxonomy.has_hierarchy: + node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang].get(classification) + context, kw = taxonomy.provide_context_and_uptodate(classification, lang, node) + kw = copy(kw) + kw["messages"] = self.site.MESSAGES + kw["translations"] = self.site.config['TRANSLATIONS'] + kw["filters"] = self.site.config['FILTERS'] + kw["site_url"] = self.site.config['SITE_URL'] + kw["blog_title"] = self.site.config['BLOG_TITLE'] + kw["generate_rss"] = self.site.config['GENERATE_RSS'] + kw["generate_atom"] = self.site.config['GENERATE_ATOM'] + kw["feed_teasers"] = self.site.config["FEED_TEASERS"] + kw["feed_plain"] = self.site.config["FEED_PLAIN"] + kw["feed_links_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"] + kw["feed_length"] = self.site.config['FEED_LENGTH'] + kw["output_folder"] = self.site.config['OUTPUT_FOLDER'] + kw["pretty_urls"] = self.site.config['PRETTY_URLS'] + kw["strip_indexes"] = self.site.config['STRIP_INDEXES'] + kw["index_file"] = self.site.config['INDEX_FILE'] + context = copy(context) + context["permalink"] = self.site.link(taxonomy.classification_name, classification, lang) + context["kind"] = taxonomy.classification_name + # Get links to other language versions of this classification + if classification_set_per_lang is not None: + other_lang_links = taxonomy.get_other_language_variants(classification, lang, classification_set_per_lang) + # Collect by language + links_per_lang = defaultdict(list) + for other_lang, link in other_lang_links: + # Make sure we ignore the current language (in case the + # plugin accidentally returns links for it as well) + if other_lang != lang: + links_per_lang[other_lang].append(link) + # Sort first by language, then by classification + sorted_links = [] + sorted_links_all = [] + for other_lang in sorted(list(links_per_lang.keys()) + [lang]): + if other_lang == lang: + sorted_links_all.append((lang, classification, taxonomy.get_classification_friendly_name(classification, lang))) + else: + links = hierarchy_utils.sort_classifications(taxonomy, links_per_lang[other_lang], other_lang) + links = [(other_lang, other_classification, + taxonomy.get_classification_friendly_name(other_classification, other_lang)) + for other_classification in links if post_lists_per_lang[other_lang].get(other_classification, ('', False, False))[1]] + sorted_links.extend(links) + sorted_links_all.extend(links) + # Store result in context and kw + context['has_other_languages'] = True + context['other_languages'] = sorted_links + context['all_languages'] = sorted_links_all + kw['other_languages'] = sorted_links + kw['all_languages'] = sorted_links_all + else: + context['has_other_languages'] = False + # Allow other plugins to modify the result + blinker.signal('generate_classification_page').send({ + 'site': self.site, + 'taxonomy': taxonomy, + 'classification': classification, + 'lang': lang, + 'posts': filtered_posts, + 'context': context, + 'kw': kw, + }) + # Decide what to do + if taxonomy.has_hierarchy and taxonomy.show_list_as_subcategories_list: + # Determine whether there are subcategories + node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang][classification] + # Are there subclassifications? + if len(node.children) > 0: + # Yes: create list with subclassifications instead of list of posts + if generate_list: + yield self._generate_subclassification_page(taxonomy, node, context, kw, lang) + return + # Generate RSS feed + if generate_rss and kw["generate_rss"] and not taxonomy.always_disable_rss: + yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], context.get("description"), kw, lang) + + # Generate Atom feed + if generate_atom and kw["generate_atom"] and not taxonomy.always_disable_atom: + yield self._generate_classification_page_as_atom(taxonomy, classification, filtered_posts, context, kw, lang) + + # Render HTML + if generate_list and taxonomy.show_list_as_index: + yield self._generate_classification_page_as_index(taxonomy, classification, filtered_posts, context, kw, lang) + elif generate_list: + yield self._generate_classification_page_as_list(taxonomy, classification, filtered_posts, context, kw, lang) + + def gen_tasks(self): + """Render the tag pages and feeds.""" + self.site.scan_posts() + yield self.group_task() + + # Cache classification sets per language for taxonomies where + # add_other_languages_variable is True. + classification_set_per_lang = {} + for taxonomy in self.site.taxonomy_plugins.values(): + if taxonomy.add_other_languages_variable: + lookup = self.site.posts_per_classification[taxonomy.classification_name] + cspl = {lang: set(lookup[lang].keys()) for lang in lookup} + classification_set_per_lang[taxonomy.classification_name] = cspl + + # Collect post lists for classification pages and determine whether + # they should be generated. + post_lists_per_lang = {} + for taxonomy in self.site.taxonomy_plugins.values(): + plpl = {} + for lang in self.site.config["TRANSLATIONS"]: + result = {} + for classification, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items(): + # Filter list + filtered_posts = self._filter_list(posts, lang) + if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications: + generate_list = generate_rss = generate_atom = False + else: + # Should we create this list? + generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang) + generate_rss = taxonomy.should_generate_rss_for_classification_page(classification, filtered_posts, lang) + generate_atom = taxonomy.should_generate_atom_for_classification_page(classification, filtered_posts, lang) + result[classification] = (filtered_posts, generate_list, generate_rss, generate_atom) + plpl[lang] = result + post_lists_per_lang[taxonomy.classification_name] = plpl + + # Now generate pages + for lang in self.site.config["TRANSLATIONS"]: + # To support that tag and category classifications share the same overview, + # we explicitly detect this case: + ignore_plugins_for_overview = set() + if 'tag' in self.site.taxonomy_plugins and 'category' in self.site.taxonomy_plugins and self.site.link("tag_index", None, lang) == self.site.link("category_index", None, lang): + # Block both plugins from creating overviews + ignore_plugins_for_overview.add(self.site.taxonomy_plugins['tag']) + ignore_plugins_for_overview.add(self.site.taxonomy_plugins['category']) + for taxonomy in self.site.taxonomy_plugins.values(): + if not taxonomy.is_enabled(lang): + continue + # Generate list of classifications (i.e. classification overview) + if taxonomy not in ignore_plugins_for_overview: + if taxonomy.template_for_classification_overview is not None: + for task in self._generate_classification_overview(taxonomy, lang): + yield task + + # Process classifications + for classification, (filtered_posts, generate_list, generate_rss, generate_atom) in post_lists_per_lang[taxonomy.classification_name][lang].items(): + for task in self._generate_classification_page(taxonomy, classification, filtered_posts, + generate_list, generate_rss, generate_atom, lang, + post_lists_per_lang[taxonomy.classification_name], + classification_set_per_lang.get(taxonomy.classification_name)): + yield task + # In case we are ignoring plugins for overview, we must have a collision for + # tags and categories. Handle this special case with extra code. + if ignore_plugins_for_overview: + for task in self._generate_tag_and_category_overview(self.site.taxonomy_plugins['tag'], self.site.taxonomy_plugins['category'], lang): + yield task diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py index d5efd61..a530db4 100644 --- a/nikola/plugins/template/__init__.py +++ b/nikola/plugins/template/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/template/jinja.plugin b/nikola/plugins/template/jinja.plugin index 78fd41b..629b20e 100644 --- a/nikola/plugins/template/jinja.plugin +++ b/nikola/plugins/template/jinja.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Support for Jinja2 templates. [Nikola] -plugincategory = Template +PluginCategory = Template diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py index 5a2135f..7795739 100644 --- a/nikola/plugins/template/jinja.py +++ b/nikola/plugins/template/jinja.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,21 +24,20 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - """Jinja template handler.""" -from __future__ import unicode_literals -import os import io import json +import os + +from nikola.plugin_categories import TemplateSystem +from nikola.utils import makedirs, req_missing, sort_posts, _smartjoin_filter + try: import jinja2 from jinja2 import meta except ImportError: - jinja2 = None # NOQA - -from nikola.plugin_categories import TemplateSystem -from nikola.utils import makedirs, req_missing + jinja2 = None class JinjaTemplates(TemplateSystem): @@ -65,6 +64,8 @@ class JinjaTemplates(TemplateSystem): self.lookup.trim_blocks = True self.lookup.lstrip_blocks = True self.lookup.filters['tojson'] = json.dumps + self.lookup.filters['sort_posts'] = sort_posts + self.lookup.filters['smartjoin'] = _smartjoin_filter self.lookup.globals['enumerate'] = enumerate self.lookup.globals['isinstance'] = isinstance self.lookup.globals['tuple'] = tuple @@ -107,7 +108,7 @@ class JinjaTemplates(TemplateSystem): """Find dependencies for a template string.""" deps = set([]) ast = self.lookup.parse(text) - dep_names = meta.find_referenced_templates(ast) + dep_names = [d for d in meta.find_referenced_templates(ast) if d] for dep_name in dep_names: filename = self.lookup.loader.get_source(self.lookup, dep_name)[1] sub_deps = [filename] + self.get_deps(filename) @@ -117,7 +118,7 @@ class JinjaTemplates(TemplateSystem): def get_deps(self, filename): """Return paths to dependencies for the template loaded from filename.""" - with io.open(filename, 'r', encoding='utf-8') as fd: + with io.open(filename, 'r', encoding='utf-8-sig') as fd: text = fd.read() return self.get_string_deps(text) diff --git a/nikola/plugins/template/mako.plugin b/nikola/plugins/template/mako.plugin index 308d291..2d353bf 100644 --- a/nikola/plugins/template/mako.plugin +++ b/nikola/plugins/template/mako.plugin @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Support for Mako templates. [Nikola] -plugincategory = Template +PluginCategory = Template diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py index 0c9bb64..30e2041 100644 --- a/nikola/plugins/template/mako.py +++ b/nikola/plugins/template/mako.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,12 +26,9 @@ """Mako template handler.""" -from __future__ import unicode_literals, print_function, absolute_import import io import os import shutil -import sys -import tempfile from mako import exceptions, util, lexer, parsetree from mako.lookup import TemplateLookup @@ -39,9 +36,9 @@ from mako.template import Template from markupsafe import Markup # It's ok, Mako requires it from nikola.plugin_categories import TemplateSystem -from nikola.utils import makedirs, get_logger, STDERR_HANDLER +from nikola.utils import makedirs, get_logger -LOGGER = get_logger('mako', STDERR_HANDLER) +LOGGER = get_logger('mako') class MakoTemplates(TemplateSystem): @@ -57,7 +54,7 @@ class MakoTemplates(TemplateSystem): def get_string_deps(self, text, filename=None): """Find dependencies for a template string.""" - lex = lexer.Lexer(text=text, filename=filename) + lex = lexer.Lexer(text=text, filename=filename, input_encoding='utf-8') lex.parse() deps = [] @@ -68,7 +65,12 @@ class MakoTemplates(TemplateSystem): # Some templates will include "foo.tmpl" and we need paths, so normalize them # using the template lookup for i, d in enumerate(deps): - deps[i] = self.get_template_path(d) + dep = self.get_template_path(d) + if dep: + deps[i] = dep + else: + LOGGER.error("Cannot find template {0} referenced in {1}", + d, filename) return deps def get_deps(self, filename): @@ -79,13 +81,6 @@ class MakoTemplates(TemplateSystem): def set_directories(self, directories, cache_folder): """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: - try: - os.path.abspath(cache_dir).decode('ascii') - except UnicodeEncodeError: - cache_dir = tempfile.mkdtemp() - LOGGER.warning('Because of a Mako bug, setting cache_dir to {0}'.format(cache_dir)) if os.path.exists(cache_dir): shutil.rmtree(cache_dir) self.directories = directories @@ -103,6 +98,7 @@ class MakoTemplates(TemplateSystem): self.lookup = TemplateLookup( directories=self.directories, module_directory=self.cache_dir, + input_encoding='utf-8', output_encoding='utf-8') def set_site(self, site): @@ -135,9 +131,10 @@ class MakoTemplates(TemplateSystem): dep_filenames = self.get_deps(template.filename) deps = [template.filename] for fname in dep_filenames: - deps += [fname] + self.get_deps(fname) - self.cache[template_name] = deps - return list(self.cache[template_name]) + # yes, it uses forward slashes on Windows + deps += self.template_deps(fname.split('/')[-1]) + self.cache[template_name] = list(set(deps)) + return self.cache[template_name] def get_template_path(self, template_name): """Get the path to a template or return None.""" diff --git a/nikola/post.py b/nikola/post.py index 37e4241..82d957d 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,36 +26,25 @@ """The Post class.""" -from __future__ import unicode_literals, print_function, absolute_import - import io -from collections import defaultdict import datetime import hashlib import json import os import re -import string -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA - -from . import utils +from collections import defaultdict +from math import ceil # for reading time feature +from urllib.parse import urljoin -from blinker import signal import dateutil.tz import lxml.html import natsort -try: - import pyphen -except ImportError: - pyphen = None - -from math import ceil # for reading time feature +from blinker import signal # for tearDown with _reload we cannot use 'from import' to get forLocaleBorg import nikola.utils +from . import metadata_extractors +from . import utils from .utils import ( current_time, Functionary, @@ -63,22 +52,37 @@ from .utils import ( LocaleBorg, slugify, to_datetime, - unicode_str, demote_headers, get_translation_candidate, - unslugify, + map_metadata ) -from .rc4 import rc4 + +try: + import pyphen +except ImportError: + pyphen = None + __all__ = ('Post',) -TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE) -_UPGRADE_METADATA_ADVERTISED = False +TEASER_REGEXP = re.compile(r'<!--\s*(TEASER_END|END_TEASER)(:(.+))?\s*-->', re.IGNORECASE) class Post(object): """Represent a blog post or site page.""" + _prev_post = None + _next_post = None + is_draft = False + is_private = False + _is_two_file = None + _reading_time = None + _remaining_reading_time = None + _paragraph_count = None + _remaining_paragraph_count = None + post_status = 'published' + has_oldstyle_metadata_tags = False + def __init__( self, source_path, @@ -87,73 +91,249 @@ class Post(object): use_in_feeds, messages, template_name, - compiler + compiler, + destination_base=None, + metadata_extractors_by=None ): """Initialize post. The source path is the user created post file. From it we calculate the meta file, as well as any translations available, and the .html fragment file path. + + destination_base must be None or a TranslatableSetting instance. If + specified, it will be prepended to the destination path. """ - self.config = config + self._load_config(config) + self._set_paths(source_path) + self.compiler = compiler - self.compile_html = self.compiler.compile_html + self.is_post = use_in_feeds + self.messages = messages + self._template_name = template_name + self.compile_html = self.compiler.compile self.demote_headers = self.compiler.demote_headers and self.config['DEMOTE_HEADERS'] - tzinfo = self.config['__tzinfo__'] + self._dependency_file_fragment = defaultdict(list) + self._dependency_file_page = defaultdict(list) + self._dependency_uptodate_fragment = defaultdict(list) + self._dependency_uptodate_page = defaultdict(list) + self._depfile = defaultdict(list) + if metadata_extractors_by is None: + self.metadata_extractors_by = {'priority': {}, 'source': {}} + else: + self.metadata_extractors_by = metadata_extractors_by + + self._set_translated_to() + self._set_folders(destination, destination_base) + + # Load default metadata + default_metadata, default_used_extractor = get_meta(self, lang=None) + self.meta = Functionary(lambda: None, self.default_lang) + self.used_extractor = Functionary(lambda: None, self.default_lang) + self.meta[self.default_lang] = default_metadata + self.used_extractor[self.default_lang] = default_used_extractor + + self._set_date(default_metadata) + + # These are the required metadata fields + if 'title' not in default_metadata or 'slug' not in default_metadata: + raise ValueError("You must set a title (found '{0}') and a slug (found '{1}')! " + "[in file {2}]".format(default_metadata.get('title', None), + default_metadata.get('slug', None), + source_path)) + + if 'type' not in default_metadata: + default_metadata['type'] = 'text' + + self._load_translated_metadata(default_metadata) + self._load_data() + self.__migrate_section_to_category() + self._set_tags() + + self.publish_later = False if self.current_time is None else self.date >= self.current_time + + # While draft comes from the tags, it's not really a tag + self.use_in_feeds = self.is_post and not self.is_draft and not self.is_private and not self.publish_later + + # Allow overriding URL_TYPE via meta + # The check is done here so meta dicts won’t change inside of + # generic_post_renderer + self.url_type = self.meta('url_type') or None + # Register potential extra dependencies + self.compiler.register_extra_dependencies(self) + + def _load_config(self, config): + """Set members to configured values.""" + self.config = config if self.config['FUTURE_IS_NOW']: self.current_time = None else: - self.current_time = current_time(tzinfo) - self.translated_to = set([]) - self._prev_post = None - self._next_post = None + self.current_time = current_time(self.config['__tzinfo__']) self.base_url = self.config['BASE_URL'] - self.is_draft = False - self.is_private = False self.strip_indexes = self.config['STRIP_INDEXES'] self.index_file = self.config['INDEX_FILE'] self.pretty_urls = self.config['PRETTY_URLS'] + self.default_lang = self.config['DEFAULT_LANG'] + self.translations = self.config['TRANSLATIONS'] + self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS'] + self._default_preview_image = self.config['DEFAULT_PREVIEW_IMAGE'] + self.types_to_hide_title = self.config['TYPES_TO_HIDE_TITLE'] + + def _set_tags(self): + """Set post tags.""" + self._tags = {} + for lang in self.translated_to: + if isinstance(self.meta[lang]['tags'], (list, tuple, set)): + _tag_list = self.meta[lang]['tags'] + else: + _tag_list = self.meta[lang]['tags'].split(',') + self._tags[lang] = natsort.natsorted( + list(set([x.strip() for x in _tag_list])), + alg=natsort.ns.F | natsort.ns.IC) + self._tags[lang] = [t for t in self._tags[lang] if t] + + status = self.meta[lang].get('status') + if status: + if status == 'published': + pass # already set before, mixing published + something else should result in the other thing + elif status == 'featured': + self.post_status = status + elif status == 'private': + self.post_status = status + self.is_private = True + elif status == 'draft': + self.post_status = status + self.is_draft = True + else: + LOGGER.warning(('The post "{0}" has the unknown status "{1}". ' + 'Valid values are "published", "featured", "private" and "draft".').format(self.source_path, status)) + + if self.config['WARN_ABOUT_TAG_METADATA']: + show_warning = False + if 'draft' in [_.lower() for _ in self._tags[lang]]: + LOGGER.warning('The post "{0}" uses the "draft" tag.'.format(self.source_path)) + show_warning = True + if 'private' in self._tags[lang]: + LOGGER.warning('The post "{0}" uses the "private" tag.'.format(self.source_path)) + show_warning = True + if 'mathjax' in self._tags[lang]: + LOGGER.warning('The post "{0}" uses the "mathjax" tag.'.format(self.source_path)) + show_warning = True + if show_warning: + LOGGER.warning('It is suggested that you convert special tags to metadata and set ' + 'USE_TAG_METADATA to False. You can use the upgrade_metadata_v8 ' + 'command plugin for conversion (install with: nikola plugin -i ' + 'upgrade_metadata_v8). Change the WARN_ABOUT_TAG_METADATA ' + 'configuration to disable this warning.') + if self.config['USE_TAG_METADATA']: + if 'draft' in [_.lower() for _ in self._tags[lang]]: + self.is_draft = True + LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path)) + self._tags[lang].remove('draft') + self.post_status = 'draft' + self.has_oldstyle_metadata_tags = True + + if 'private' in self._tags[lang]: + self.is_private = True + LOGGER.debug('The post "{0}" is private.'.format(self.source_path)) + self._tags[lang].remove('private') + self.post_status = 'private' + self.has_oldstyle_metadata_tags = True + + if 'mathjax' in self._tags[lang]: + self.has_oldstyle_metadata_tags = True + + def _set_paths(self, source_path): + """Set the various paths and the post_name. + + TODO: WTF is all this. + """ self.source_path = source_path # posts/blah.txt self.post_name = os.path.splitext(source_path)[0] # posts/blah + _relpath = os.path.relpath(self.post_name) + if _relpath != self.post_name: + self.post_name = _relpath.replace('..' + os.sep, '_..' + os.sep) # cache[\/]posts[\/]blah.html self.base_path = os.path.join(self.config['CACHE_FOLDER'], self.post_name + ".html") # cache/posts/blah.html self._base_path = self.base_path.replace('\\', '/') self.metadata_path = self.post_name + ".meta" # posts/blah.meta - self.folder = destination - self.translations = self.config['TRANSLATIONS'] - self.default_lang = self.config['DEFAULT_LANG'] - self.messages = messages - self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS'] - self._template_name = template_name - self.is_two_file = True - self.newstylemeta = True - self._reading_time = None - self._remaining_reading_time = None - self._paragraph_count = None - self._remaining_paragraph_count = None - self._dependency_file_fragment = defaultdict(list) - self._dependency_file_page = defaultdict(list) - self._dependency_uptodate_fragment = defaultdict(list) - self._dependency_uptodate_page = defaultdict(list) - self._depfile = defaultdict(list) - - default_metadata, self.newstylemeta = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES']) - - self.meta = Functionary(lambda: None, self.default_lang) - self.meta[self.default_lang] = default_metadata - # Load internationalized metadata + def _set_translated_to(self): + """Find post's translations.""" + self.translated_to = set([]) for lang in self.translations: if os.path.isfile(get_translation_candidate(self.config, self.source_path, lang)): self.translated_to.add(lang) + + # If we don't have anything in translated_to, the file does not exist + if not self.translated_to and os.path.isfile(self.source_path): + raise Exception(("Could not find translations for {}, check your " + "TRANSLATIONS_PATTERN").format(self.source_path)) + elif not self.translated_to: + raise Exception(("Cannot use {} (not a file, perhaps a broken " + "symbolic link?)").format(self.source_path)) + + def _set_folders(self, destination, destination_base): + """Compose destination paths.""" + self.folder_relative = destination + self.folder_base = destination_base + + if self.folder_base is not None: + # Use translatable destination folders + self.folders = {} + for lang in self.config['TRANSLATIONS']: + if os.path.isabs(self.folder_base(lang)): # Issue 2982 + self.folder_base[lang] = os.path.relpath(self.folder_base(lang), '/') + self.folders[lang] = os.path.normpath(os.path.join(self.folder_base(lang), self.folder_relative)) + else: + # Old behavior (non-translatable destination path, normalized by scanner) + self.folders = {lang: self.folder_relative for lang in self.config['TRANSLATIONS'].keys()} + self.folder = self.folders[self.default_lang] + + def __migrate_section_to_category(self): + """TODO: remove in v9.""" + for lang, meta in self.meta.items(): + # Migrate section to category + # TODO: remove in v9 + if 'section' in meta: + if 'category' in meta: + LOGGER.warning("Post {0} has both 'category' and 'section' metadata. Section will be ignored.".format(self.source_path)) + else: + meta['category'] = meta['section'] + LOGGER.info("Post {0} uses 'section' metadata, setting its value to 'category'".format(self.source_path)) + + # Handle CATEGORY_DESTPATH_AS_DEFAULT + if 'category' not in meta and self.config['CATEGORY_DESTPATH_AS_DEFAULT']: + self.category_from_destpath = True + if self.config['CATEGORY_DESTPATH_TRIM_PREFIX'] and self.folder_relative != '.': + category = self.folder_relative + else: + category = self.folders[lang] + category = category.replace(os.sep, '/') + if self.config['CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY']: + category = category.split('/')[0] + meta['category'] = self.config['CATEGORY_DESTPATH_NAMES'](lang).get(category, category) + else: + self.category_from_destpath = False + + def _load_data(self): + """Load data field from metadata.""" + self.data = Functionary(lambda: None, self.default_lang) + for lang in self.translations: + if self.meta[lang].get('data') is not None: + self.data[lang] = utils.load_data(self.meta[lang]['data']) + + def _load_translated_metadata(self, default_metadata): + """Load metadata from all translation sources.""" + for lang in self.translations: if lang != self.default_lang: meta = defaultdict(lambda: '') meta.update(default_metadata) - _meta, _nsm = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'], lang) - self.newstylemeta = self.newstylemeta and _nsm + _meta, _extractors = get_meta(self, lang) meta.update(_meta) self.meta[lang] = meta + self.used_extractor[lang] = _extractors if not self.is_translation_available(self.default_lang): # Special case! (Issue #373) @@ -161,91 +341,80 @@ class Post(object): for lang in sorted(self.translated_to): default_metadata.update(self.meta[lang]) - # Load data field from metadata - self.data = Functionary(lambda: None, self.default_lang) - for lang in self.translations: - if self.meta[lang].get('data') is not None: - self.data[lang] = utils.load_data(self.meta[lang]['data']) - - if 'date' not in default_metadata and not use_in_feeds: + def _set_date(self, default_metadata): + """Set post date/updated based on metadata and configuration.""" + if 'date' not in default_metadata and not self.is_post: # For pages we don't *really* need a date if self.config['__invariant__']: - default_metadata['date'] = datetime.datetime(2013, 12, 31, 23, 59, 59, tzinfo=tzinfo) + default_metadata['date'] = datetime.datetime(2013, 12, 31, 23, 59, 59, tzinfo=self.config['__tzinfo__']) else: default_metadata['date'] = datetime.datetime.utcfromtimestamp( - os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(tzinfo) + os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(self.config['__tzinfo__']) # If time zone is set, build localized datetime. try: - self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo) + self.date = to_datetime(self.meta[self.default_lang]['date'], self.config['__tzinfo__']) except ValueError: - raise ValueError("Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], source_path)) + if not self.meta[self.default_lang]['date']: + msg = 'Missing date in file {}'.format(self.source_path) + else: + msg = "Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], self.source_path) + LOGGER.error(msg) + raise ValueError(msg) if 'updated' not in default_metadata: default_metadata['updated'] = default_metadata.get('date', None) - self.updated = to_datetime(default_metadata['updated']) + self.updated = to_datetime(default_metadata['updated'], self.config['__tzinfo__']) - if 'title' not in default_metadata or 'slug' not in default_metadata \ - or 'date' not in default_metadata: - raise ValueError("You must set a title (found '{0}'), a slug (found '{1}') and a date (found '{2}')! " - "[in file {3}]".format(default_metadata.get('title', None), - default_metadata.get('slug', None), - default_metadata.get('date', None), - source_path)) - - if 'type' not in default_metadata: - # default value is 'text' - default_metadata['type'] = 'text' - - self.publish_later = False if self.current_time is None else self.date >= self.current_time - - is_draft = False - is_private = False - self._tags = {} - for lang in self.translated_to: - self._tags[lang] = natsort.natsorted( - list(set([x.strip() for x in self.meta[lang]['tags'].split(',')])), - alg=natsort.ns.F | natsort.ns.IC) - self._tags[lang] = [t for t in self._tags[lang] if t] - if 'draft' in [_.lower() for _ in self._tags[lang]]: - is_draft = True - LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path)) - self._tags[lang].remove('draft') - - # TODO: remove in v8 - if 'retired' in self._tags[lang]: - is_private = True - LOGGER.warning('The "retired" tag in post "{0}" is now deprecated and will be removed in v8. Use "private" instead.'.format(self.source_path)) - self._tags[lang].remove('retired') - # end remove in v8 - - if 'private' in self._tags[lang]: - is_private = True - LOGGER.debug('The post "{0}" is private.'.format(self.source_path)) - self._tags[lang].remove('private') + @property + def hyphenate(self): + """Post is hyphenated.""" + return bool(self.config['HYPHENATE'] or self.meta('hyphenate')) - # While draft comes from the tags, it's not really a tag - self.is_draft = is_draft - self.is_private = is_private - self.is_post = use_in_feeds - self.use_in_feeds = use_in_feeds and not is_draft and not is_private \ - and not self.publish_later + @property + def is_two_file(self): + """Post has a separate .meta file.""" + if self._is_two_file is None: + return True + return self._is_two_file - # Register potential extra dependencies - self.compiler.register_extra_dependencies(self) + @is_two_file.setter + def is_two_file(self, value): + """Set the is_two_file property, use with care. - def _get_hyphenate(self): - return bool(self.config['HYPHENATE'] or self.meta('hyphenate')) + Caution: this MAY REWRITE THE POST FILE. + Only should happen if you effectively *change* the value. - hyphenate = property(_get_hyphenate) + Arguments: + value {bool} -- Whether the post has a separate .meta file + """ + # for lang in self.translated_to: + + if self._is_two_file is None: + # Initial setting, this happens on post creation + self._is_two_file = value + elif value != self._is_two_file: + # Changing the value, this means you are transforming a 2-file + # into a 1-file or viceversa. + if value and not self.compiler.supports_metadata: + raise ValueError("Can't save metadata as 1-file using this compiler {}".format(self.compiler)) + for lang in self.translated_to: + source = self.source(lang) + meta = self.meta(lang) + self._is_two_file = value + self.save(lang=lang, source=source, meta=meta) + if not value: # Need to delete old meta file + meta_path = get_translation_candidate(self.config, self.metadata_path, lang) + if os.path.isfile(meta_path): + os.unlink(meta_path) 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) - m.update(utils.unicode_str(os.stat(self.source_path).st_mtime).encode('utf-8')) + m.update(str(os.stat(self.source_path).st_mtime).encode('utf-8')) clean_meta = {} for k, v in self.meta.items(): sub_meta = {} @@ -253,27 +422,45 @@ class Post(object): for kk, vv in v.items(): if vv: sub_meta[kk] = vv - m.update(utils.unicode_str(json.dumps(clean_meta, cls=utils.CustomEncoder, sort_keys=True)).encode('utf-8')) + m.update(str(json.dumps(clean_meta, cls=utils.CustomEncoder, sort_keys=True)).encode('utf-8')) return '<Post: {0!r} {1}>'.format(self.source_path, m.hexdigest()) - def _has_pretty_url(self, lang): - if self.pretty_urls and \ - self.meta[lang].get('pretty_url', '') != 'False' and \ - self.meta[lang]['slug'] != 'index': - return True + def has_pretty_url(self, lang): + """Check if this page has a pretty URL.""" + m = self.meta[lang].get('pretty_url', '') + if m: + # match is a non-empty string, overides anything + return m.lower() == 'true' or m.lower() == 'yes' else: - return False + # use PRETTY_URLS, unless the slug is 'index' + return self.pretty_urls and self.meta[lang]['slug'] != 'index' + + def _has_pretty_url(self, lang): + """Check if this page has a pretty URL.""" + return self.has_pretty_url(lang) @property - def is_mathjax(self): - """True if this post has the mathjax tag in the current language or is a python notebook.""" + def has_math(self): + """Return True if this post has has_math set to True or is a python notebook. + + Alternatively, it will return True if it has set the mathjax tag in the + current language and the USE_TAG_METADATA config setting is True. + """ if self.compiler.name == 'ipynb': return True lang = nikola.utils.LocaleBorg().current_lang if self.is_translation_available(lang): - return 'mathjax' in self.tags_for_language(lang) + if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True): + return True + if self.config['USE_TAG_METADATA']: + return 'mathjax' in self.tags_for_language(lang) # If it has math in ANY other language, enable it. Better inefficient than broken. - return 'mathjax' in self.alltags + for lang in self.translated_to: + if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True): + return True + if self.config['USE_TAG_METADATA']: + return 'mathjax' in self.alltags + return False @property def alltags(self): @@ -313,7 +500,7 @@ class Post(object): rv = rv._prev_post return rv - @prev_post.setter # NOQA + @prev_post.setter def prev_post(self, v): """Set previous post.""" self._prev_post = v @@ -331,7 +518,7 @@ class Post(object): rv = rv._next_post return rv - @next_post.setter # NOQA + @next_post.setter def next_post(self, v): """Set next post.""" self._next_post = v @@ -343,11 +530,11 @@ class Post(object): return self.meta[lang]['template'] or self._template_name def formatted_date(self, date_format, date=None): - """Return the formatted date as unicode.""" + """Return the formatted date as string.""" return utils.LocaleBorg().formatted_date(date_format, date if date else self.date) def formatted_updated(self, date_format): - """Return the updated date as unicode.""" + """Return the updated date as string.""" return self.formatted_date(date_format, self.updated) def title(self, lang=None): @@ -360,7 +547,7 @@ class Post(object): lang = nikola.utils.LocaleBorg().current_lang return self.meta[lang]['title'] - def author(self, lang=None): + def author(self, lang=None) -> str: """Return localized author or BLOG_AUTHOR if unspecified. If lang is not specified, it defaults to the current language from @@ -375,12 +562,38 @@ class Post(object): return author + def authors(self, lang=None) -> list: + """Return localized authors or BLOG_AUTHOR if unspecified. + + If lang is not specified, it defaults to the current language from + templates, as set in LocaleBorg. + """ + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + if self.meta[lang]['author']: + author = [i.strip() for i in self.meta[lang]['author'].split(",")] + else: + author = [self.config['BLOG_AUTHOR'](lang)] + + return author + def description(self, lang=None): """Return localized description.""" if lang is None: lang = nikola.utils.LocaleBorg().current_lang return self.meta[lang]['description'] + def guid(self, lang=None): + """Return localized GUID.""" + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + if self.meta[lang]['guid']: + guid = self.meta[lang]['guid'] + else: + guid = self.permalink(lang, absolute=True) + + return guid + def add_dependency(self, dependency, add='both', lang=None): """Add a file dependency for tasks using that post. @@ -440,12 +653,15 @@ class Post(object): self._depfile[dest].append(dep) @staticmethod - def write_depfile(dest, deps_list): + def write_depfile(dest, deps_list, post=None, lang=None): """Write a depfile for a given language.""" - deps_path = dest + '.dep' - if deps_list: + if post is None or lang is None: + deps_path = dest + '.dep' + else: + deps_path = post.compiler.get_dep_filename(post, lang) + if deps_list or (post.compiler.use_dep_file if post else False): deps_list = [p for p in deps_list if p != dest] # Don't depend on yourself (#1671) - with io.open(deps_path, "w+", encoding="utf8") as deps_file: + with io.open(deps_path, "w+", encoding="utf-8") as deps_file: deps_file.write('\n'.join(deps_list)) else: if os.path.isfile(deps_path): @@ -470,11 +686,10 @@ class Post(object): def deps(self, lang): """Return a list of file dependencies to build this post's page.""" deps = [] - if self.default_lang in self.translated_to: - deps.append(self.base_path) - deps.append(self.source_path) - if os.path.exists(self.metadata_path): - deps.append(self.metadata_path) + deps.append(self.base_path) + deps.append(self.source_path) + if os.path.exists(self.metadata_path): + deps.append(self.metadata_path) if lang != self.default_lang: cand_1 = get_translation_candidate(self.config, self.source_path, lang) cand_2 = get_translation_candidate(self.config, self.base_path, lang) @@ -487,7 +702,7 @@ class Post(object): deps.append(self.meta('data', lang)) deps += self._get_dependencies(self._dependency_file_page[lang]) deps += self._get_dependencies(self._dependency_file_page[None]) - return sorted(deps) + return sorted(set(deps)) def deps_uptodate(self, lang): """Return a list of uptodate dependencies to build this post's page. @@ -503,14 +718,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: - data = inf.read() + "<!--tail-->" - data = CRYPT.substitute(data=rc4(password, data)) - with io.open(path, 'w+', encoding='utf8') as outf: - outf.write(data) - dest = self.translated_base_path(lang) if not self.is_translation_available(lang) and not self.config['SHOW_UNTRANSLATED_POSTS']: return @@ -519,34 +726,25 @@ class Post(object): self.compile_html( self.translated_source_path(lang), dest, - self.is_two_file) - Post.write_depfile(dest, self._depfile[dest]) + self.is_two_file, + self, + lang) + Post.write_depfile(dest, self._depfile[dest], post=self, lang=lang) signal('compiled').send({ 'source': self.translated_source_path(lang), 'dest': dest, 'post': self, + 'lang': lang, }) - if self.meta('password'): - # TODO: get rid of this feature one day (v8?; warning added in v7.3.0.) - LOGGER.warn("The post {0} is using the `password` attribute, which may stop working in the future.") - LOGGER.warn("Please consider switching to a more secure method of encryption.") - LOGGER.warn("More details: https://github.com/getnikola/nikola/issues/1547") - wrap_encrypt(dest, self.meta('password')) if self.publish_later: - LOGGER.notice('{0} is scheduled to be published in the future ({1})'.format( + LOGGER.info('{0} is scheduled to be published in the future ({1})'.format( self.source_path, self.date)) def fragment_deps(self, lang): - """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. - """ - deps = [] - if self.default_lang in self.translated_to: - deps.append(self.source_path) + """Return a list of dependencies to build this post's fragment.""" + deps = [self.source_path] if os.path.isfile(self.metadata_path): deps.append(self.metadata_path) lang_deps = [] @@ -587,20 +785,90 @@ class Post(object): return get_translation_candidate(self.config, self.base_path, lang) def _translated_file_path(self, lang): - """Return path to the translation's file, or to the original.""" + """Get path to a post's translation. + + Returns path to the translation's file, or to as good a file as it can + plus "real" language of the text. + """ if lang in self.translated_to: if lang == self.default_lang: - return self.base_path + return self.base_path, lang else: - return get_translation_candidate(self.config, self.base_path, lang) + return get_translation_candidate(self.config, self.base_path, lang), lang elif lang != self.default_lang: - return self.base_path + return self.base_path, self.default_lang else: - return get_translation_candidate(self.config, self.base_path, sorted(self.translated_to)[0]) + real_lang = sorted(self.translated_to)[0] + return get_translation_candidate(self.config, self.base_path, real_lang), real_lang + + def write_metadata(self, lang=None): + """Save the post's metadata. + + Keep in mind that this will save either in the + post file or in a .meta file, depending on self.is_two_file. + + metadata obtained from filenames or document contents will + be superseded by this, and becomes inaccessible. + + Post contents will **not** be modified. + + If you write to a language not in self.translated_to + an exception will be raised. + + Remember to scan_posts(really=True) after you update metadata if + you want the rest of the system to know about the change. + """ + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + if lang not in self.translated_to: + raise ValueError("Can't save post metadata to language [{}] it's not translated to.".format(lang)) + + source = self.source(lang) + source_path = self.translated_source_path(lang) + metadata = self.meta[lang] + self.compiler.create_post(source_path, content=source, onefile=not self.is_two_file, is_page=not self.is_post, **metadata) + + def save(self, lang=None, source=None, meta=None): + """Write post source to disk. + + Use this with utmost care, it may wipe out a post. + + Keyword Arguments: + lang str -- Language for this source. If set to None, + use current language. + source str -- The source text for the post in the + language. If set to None, use current source for + this language. + meta dict -- Metadata for this language, if not set, + use current metadata for this language. + """ + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + if source is None: + source = self.source(lang) + if meta is None: + metadata = self.meta[lang] + source_path = self.translated_source_path(lang) + metadata = self.meta[lang] + self.compiler.create_post(source_path, content=source, onefile=not self.is_two_file, is_page=not self.is_post, **metadata) + + def source(self, lang=None): + """Read the post and return its source.""" + if lang is None: + lang = nikola.utils.LocaleBorg().current_lang + + source = self.translated_source_path(lang) + with open(source, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + if self.is_two_file: # Metadata is not here + source_data = data + else: + source_data = self.compiler.split_metadata(data, self, lang)[1] + return source_data def text(self, lang=None, teaser_only=False, strip_html=False, show_read_more_link=True, feed_read_more_link=False, feed_links_append_query=None): - """Read the post file for that language and return its contents. + """Read the post file for that language and return its compiled contents. teaser_only=True breaks at the teaser marker and returns only the teaser. strip_html=True removes HTML tags @@ -613,7 +881,7 @@ class Post(object): """ if lang is None: lang = nikola.utils.LocaleBorg().current_lang - file_name = self._translated_file_path(lang) + file_name, real_lang = self._translated_file_path(lang) # Yes, we compile it and screw it. # This may be controversial, but the user (or someone) is asking for the post text @@ -621,7 +889,7 @@ class Post(object): if not os.path.isfile(file_name): self.compile(lang) - with io.open(file_name, "r", encoding="utf8") as post_file: + with io.open(file_name, "r", encoding="utf-8-sig") as post_file: data = post_file.read().strip() if self.compiler.extension() == '.php': @@ -633,16 +901,16 @@ class Post(object): if str(e) == "Document is empty": return "" # let other errors raise - raise(e) + raise base_url = self.permalink(lang=lang) document.make_links_absolute(base_url) if self.hyphenate: - hyphenate(document, lang) + hyphenate(document, real_lang) try: data = lxml.html.tostring(document.body, encoding='unicode') - except: + except Exception: data = lxml.html.tostring(document, encoding='unicode') if teaser_only: @@ -662,7 +930,8 @@ class Post(object): reading_time=self.reading_time, remaining_reading_time=self.remaining_reading_time, paragraph_count=self.paragraph_count, - remaining_paragraph_count=self.remaining_paragraph_count) + remaining_paragraph_count=self.remaining_paragraph_count, + post_title=self.title(lang)) # This closes all open tags and sanitizes the broken HTML document = lxml.html.fromstring(teaser) try: @@ -675,7 +944,7 @@ class Post(object): # Not all posts have a body. For example, you may have a page statically defined in the template that does not take content as input. content = lxml.html.fromstring(data) data = content.text_content().strip() # No whitespace wanted. - except lxml.etree.ParserError: + except (lxml.etree.ParserError, ValueError): data = "" elif data: if self.demote_headers: @@ -691,7 +960,7 @@ class Post(object): @property def reading_time(self): - """Reading time based on length of text.""" + """Return reading time based on length of text.""" if self._reading_time is None: text = self.text(strip_html=True) words_per_minute = 220 @@ -720,8 +989,8 @@ class Post(object): if self._paragraph_count is None: # duplicated with Post.text() lang = nikola.utils.LocaleBorg().current_lang - file_name = self._translated_file_path(lang) - with io.open(file_name, "r", encoding="utf8") as post_file: + file_name, _ = self._translated_file_path(lang) + with io.open(file_name, "r", encoding="utf-8-sig") as post_file: data = post_file.read().strip() try: document = lxml.html.fragment_fromstring(data, "body") @@ -730,7 +999,7 @@ class Post(object): if str(e) == "Document is empty": return "" # let other errors raise - raise(e) + raise # output is a float, for no real reason at all self._paragraph_count = int(document.xpath('count(//p)')) @@ -748,7 +1017,7 @@ class Post(object): if str(e) == "Document is empty": return "" # let other errors raise - raise(e) + raise self._remaining_paragraph_count = self.paragraph_count - int(document.xpath('count(//p)')) return self._remaining_paragraph_count @@ -768,73 +1037,19 @@ class Post(object): """ if lang is None: lang = nikola.utils.LocaleBorg().current_lang - if self._has_pretty_url(lang): + folder = self.folders[lang] + if self.has_pretty_url(lang): path = os.path.join(self.translations[lang], - self.folder, self.meta[lang]['slug'], 'index' + extension) + folder, self.meta[lang]['slug'], 'index' + extension) else: path = os.path.join(self.translations[lang], - self.folder, self.meta[lang]['slug'] + extension) + folder, self.meta[lang]['slug'] + extension) if sep != os.sep: path = path.replace(os.sep, sep) if path.startswith('./'): path = path[2:] return path - def section_color(self, lang=None): - """Return the color of the post's section.""" - slug = self.section_slug(lang) - if slug in self.config['POSTS_SECTION_COLORS'](lang): - return self.config['POSTS_SECTION_COLORS'](lang)[slug] - base = self.config['THEME_COLOR'] - return utils.colorize_str_from_base_color(slug, base) - - def section_link(self, lang=None): - """Return the link to the post's section (deprecated).""" - utils.LOGGER.warning("Post.section_link is deprecated. Please use " + - "site.link('section_index', post.section_slug()) instead.") - if lang is None: - lang = nikola.utils.LocaleBorg().current_lang - - slug = self.section_slug(lang) - t = os.path.normpath(self.translations[lang]) - if t == '.': - t = '' - link = '/' + '/'.join(i for i in (t, slug) if i) + '/' - if not self.pretty_urls: - link = urljoin(link, self.index_file) - link = utils.encodelink(link) - return link - - def section_name(self, lang=None): - """Return the name of the post's section.""" - slug = self.section_slug(lang) - if slug in self.config['POSTS_SECTION_NAME'](lang): - name = self.config['POSTS_SECTION_NAME'](lang)[slug] - else: - name = slug.replace('-', ' ').title() - return name - - def section_slug(self, lang=None): - """Return the slug for the post's section.""" - if lang is None: - lang = nikola.utils.LocaleBorg().current_lang - - if not self.config['POSTS_SECTION_FROM_META']: - dest = self.destination_path(lang) - if dest[-(1 + len(self.index_file)):] == os.sep + self.index_file: - dest = dest[:-(1 + len(self.index_file))] - dirname = os.path.dirname(dest) - slug = dest.split(os.sep) - if not slug or dirname == '.': - slug = self.messages[lang]["Uncategorized"] - elif lang == slug[0]: - slug = slug[1] - else: - slug = slug[0] - else: - slug = self.meta[lang]['section'].split(',')[0] if 'section' in self.meta[lang] else self.messages[lang]["Uncategorized"] - return utils.slugify(slug, lang) - def permalink(self, lang=None, absolute=False, extension='.html', query=None): """Return permalink for a post.""" if lang is None: @@ -845,8 +1060,8 @@ class Post(object): extension = self.compiler.extension() pieces = self.translations[lang].split(os.sep) - pieces += self.folder.split(os.sep) - if self._has_pretty_url(lang): + pieces += self.folders[lang].split(os.sep) + if self.has_pretty_url(lang): pieces += [self.meta[lang]['slug'], 'index' + extension] else: pieces += [self.meta[lang]['slug'] + extension] @@ -869,13 +1084,14 @@ class Post(object): lang = nikola.utils.LocaleBorg().current_lang image_path = self.meta[lang]['previewimage'] - if not image_path: - return None + image_path = self._default_preview_image - # This is further parsed by the template, because we don’t have access - # to the URL replacer here. (Issue #1473) - return image_path + if not image_path or image_path.startswith("/"): + # Paths starting with slashes are expected to be root-relative, pass them directly. + return image_path + # Other paths are relative to the permalink. The path will be made prettier by the URL replacer later. + return urljoin(self.permalink(lang), image_path) def source_ext(self, prefix=False): """Return the source file extension. @@ -891,47 +1107,17 @@ class Post(object): else: return ext -# Code that fetches metadata from different places + def should_hide_title(self): + """Return True if this post's title should be hidden. Use in templates to manage posts without titles.""" + return self.title().strip() in ('NO TITLE', '') or self.meta('hidetitle') or \ + self.meta('type').strip() in self.types_to_hide_title + def should_show_title(self): + """Return True if this post's title should be displayed. Use in templates to manage posts without titles.""" + return not self.should_hide_title() -def re_meta(line, match=None): - """Find metadata using regular expressions.""" - if match: - reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match))) - else: - reStr = re.compile('^\.\. (.*?): (.*)') - result = reStr.findall(line.strip()) - if match and result: - return (match, result[0]) - elif not match and result: - return (result[0][0], result[0][1].strip()) - else: - return (None,) - - -def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_titles, lang): - """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 - """ - match = re.match(metadata_regexp, filename) - meta = {} - - if match: - # .items() for py3k compat. - for key, value in match.groupdict().items(): - k = key.lower().strip() # metadata must be lowercase - if k == 'title' and unslugify_titles: - meta[k] = unslugify(value, lang, discard_numbers=False) - else: - meta[k] = value - - return meta - - -def get_metadata_from_file(source_path, config=None, lang=None): +def get_metadata_from_file(source_path, post, config, lang, metadata_extractors_by): """Extract metadata from the file itself, by parsing contents.""" try: if lang and config: @@ -939,183 +1125,113 @@ def get_metadata_from_file(source_path, config=None, lang=None): elif lang: source_path += '.' + lang with io.open(source_path, "r", encoding="utf-8-sig") as meta_file: - meta_data = [x.strip() for x in meta_file.readlines()] - return _get_metadata_from_file(meta_data) + source_text = meta_file.read() except (UnicodeDecodeError, UnicodeEncodeError): - raise ValueError('Error reading {0}: Nikola only supports UTF-8 files'.format(source_path)) + msg = 'Error reading {0}: Nikola only supports UTF-8 files'.format(source_path) + LOGGER.error(msg) + raise ValueError(msg) except Exception: # The file may not exist, for multilingual sites - return {} - - -re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#'))) -# Assuming rst titles are going to be at least 4 chars long -# otherwise this detects things like ''' wich breaks other markups. -re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape( - string.punctuation))) - - -def _get_title_from_contents(meta_data): - """Extract title from file contents, LAST RESOURCE.""" - piece = meta_data[:] - title = None - for i, line in enumerate(piece): - if re_rst_title.findall(line) and i > 0: - title = meta_data[i - 1].strip() - break - if (re_rst_title.findall(line) and i >= 0 and - re_rst_title.findall(meta_data[i + 2])): - title = meta_data[i + 1].strip() - break - if re_md_title.findall(line): - title = re_md_title.findall(line)[0] - break - return title - + return {}, None -def _get_metadata_from_file(meta_data): - """Extract metadata from a post's source file.""" meta = {} - if not meta_data: - return meta - - # 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 + used_extractor = None + for priority in metadata_extractors.MetaPriority: + found_in_priority = False + for extractor in metadata_extractors_by['priority'].get(priority, []): + if not metadata_extractors.check_conditions(post, source_path, extractor.conditions, config, source_text): + continue + extractor.check_requirements() + new_meta = extractor.extract_text(source_text) + if new_meta: + found_in_priority = True + used_extractor = extractor + # Map metadata from other platforms to names Nikola expects (Issue #2817) + # Map metadata values (Issue #3025) + map_metadata(new_meta, extractor.map_from, config) + + meta.update(new_meta) + break - for i, line in enumerate(meta_data): - if not line: + if found_in_priority: break - match = re_meta(line) - if match[0]: - meta[match[0]] = match[1] + return meta, used_extractor - # If we have no title, try to get it from document - if 'title' not in meta: - t = _get_title_from_contents(meta_data) - if t is not None: - meta['title'] = t - return meta - - -def get_metadata_from_meta_file(path, config=None, lang=None): +def get_metadata_from_meta_file(path, post, config, lang, metadata_extractors_by=None): """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: meta_path = get_translation_candidate(config, meta_path, lang) elif lang: meta_path += '.' + lang if os.path.isfile(meta_path): - with io.open(meta_path, "r", encoding="utf8") as meta_file: - meta_data = meta_file.readlines() - - # Detect new-style metadata. - newstyleregexp = re.compile(r'\.\. .*?: .*') - newstylemeta = False - for l in meta_data: - if l.strip(): - if re.match(newstyleregexp, l): - newstylemeta = True - - if newstylemeta: - # New-style metadata is basically the same as reading metadata from - # a 1-file post. - return get_metadata_from_file(path, config, lang), newstylemeta - else: - if not _UPGRADE_METADATA_ADVERTISED: - LOGGER.warn("Some posts on your site have old-style metadata. You should upgrade them to the new format, with support for extra fields.") - LOGGER.warn("Install the 'upgrade_metadata' plugin (with 'nikola plugin -i upgrade_metadata') and run 'nikola upgrade_metadata'.") - _UPGRADE_METADATA_ADVERTISED = True - while len(meta_data) < 7: - meta_data.append("") - (title, slug, date, tags, link, description, _type) = [ - x.strip() for x in meta_data][:7] - - meta = {} - - if title: - meta['title'] = title - if slug: - meta['slug'] = slug - if date: - meta['date'] = date - if tags: - meta['tags'] = tags - if link: - meta['link'] = link - if description: - meta['description'] = description - if _type: - meta['type'] = _type - - return meta, newstylemeta - + return get_metadata_from_file(meta_path, post, config, lang, metadata_extractors_by) elif lang: # Metadata file doesn't exist, but not default language, # So, if default language metadata exists, return that. # This makes the 2-file format detection more reliable (Issue #525) - return get_metadata_from_meta_file(path, config, lang=None) - else: - return {}, True - + return get_metadata_from_meta_file(meta_path, post, config, None, metadata_extractors_by) + else: # No 2-file metadata + return {}, None -def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None): - """Get post's meta from source. - If ``file_metadata_regexp`` is given it will be tried to read - metadata from the filename. - If ``unslugify_titles`` is True, the extracted title (if any) will be unslugified, as is done in galleries. - If any metadata is then found inside the file the metadata from the - file will override previous findings. - """ +def get_meta(post, lang): + """Get post meta from compiler or source file.""" meta = defaultdict(lambda: '') + used_extractor = None - try: - config = post.config - except AttributeError: - config = None + config = getattr(post, 'config', None) + metadata_extractors_by = getattr(post, 'metadata_extractors_by') + if metadata_extractors_by is None: + metadata_extractors_by = metadata_extractors.default_metadata_extractors_by() - _, newstylemeta = get_metadata_from_meta_file(post.metadata_path, config, lang) - meta.update(_) + # If meta file exists, use it + metafile_meta, used_extractor = get_metadata_from_meta_file(post.metadata_path, post, config, lang, metadata_extractors_by) - if not meta: - post.is_two_file = False + is_two_file = bool(metafile_meta) - if file_metadata_regexp is not None: - meta.update(_get_metadata_from_filename_by_regex(post.source_path, - file_metadata_regexp, - unslugify_titles, - post.default_lang)) + # Filename-based metadata extractors (priority 1). + if config.get('FILE_METADATA_REGEXP'): + extractors = metadata_extractors_by['source'].get(metadata_extractors.MetaSource.filename, []) + for extractor in extractors: + if not metadata_extractors.check_conditions(post, post.source_path, extractor.conditions, config, None): + continue + meta.update(extractor.extract_filename(post.source_path, lang)) + # Fetch compiler metadata (priority 2, overrides filename-based metadata). compiler_meta = {} - if getattr(post, 'compiler', None): - compiler_meta = post.compiler.read_metadata(post, file_metadata_regexp, unslugify_titles, lang) + if (getattr(post, 'compiler', None) and post.compiler.supports_metadata and + metadata_extractors.check_conditions(post, post.source_path, post.compiler.metadata_conditions, config, None)): + compiler_meta = post.compiler.read_metadata(post, lang=lang) + used_extractor = post.compiler meta.update(compiler_meta) - 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)) + # Meta files and inter-file metadata (priority 3, overrides compiler and filename-based metadata). + if not metafile_meta: + new_meta, used_extractor = get_metadata_from_file(post.source_path, post, config, lang, metadata_extractors_by) + meta.update(new_meta) + else: + meta.update(metafile_meta) if lang is None: # Only perform these checks for the default language - if 'slug' not in meta: # If no slug is found in the metadata use the filename - meta['slug'] = slugify(unicode_str(os.path.splitext( - os.path.basename(post.source_path))[0]), post.default_lang) + meta['slug'] = slugify(os.path.splitext( + os.path.basename(post.source_path))[0], post.default_lang) if 'title' not in meta: # If no title is found, use the filename without extension meta['title'] = os.path.splitext( os.path.basename(post.source_path))[0] - return meta, newstylemeta + # Set one-file status basing on default language only (Issue #3191) + if is_two_file or lang is None: + # Direct access because setter is complicated + post._is_two_file = is_two_file + + return meta, used_extractor def hyphenate(dom, _lang): @@ -1140,10 +1256,11 @@ def hyphenate(dom, _lang): for tag in ('p', 'li', 'span'): for node in dom.xpath("//%s[not(parent::pre)]" % tag): skip_node = False - skippable_nodes = ['kbd', 'code', 'samp', 'mark', 'math', 'data', 'ruby', 'svg'] + skippable_nodes = ['kbd', 'pre', 'code', 'samp', 'mark', 'math', 'data', 'ruby', 'svg'] if node.getchildren(): for child in node.getchildren(): - if child.tag in skippable_nodes or (child.tag == 'span' and 'math' in child.get('class', [])): + if child.tag in skippable_nodes or (child.tag == 'span' and 'math' + in child.get('class', [])): skip_node = True elif 'math' in node.get('class', []): skip_node = True @@ -1162,8 +1279,14 @@ def insert_hyphens(node, hyphenator): text = getattr(node, attr) if not text: continue - new_data = ' '.join([hyphenator.inserted(w, hyphen='\u00AD') - for w in text.split(' ')]) + + lines = text.splitlines() + new_data = "\n".join( + [ + " ".join([hyphenator.inserted(w, hyphen="\u00AD") for w in line.split(" ")]) + for line in lines + ] + ) # Spaces are trimmed, we have to add them manually back if text[0].isspace(): new_data = ' ' + new_data @@ -1173,53 +1296,3 @@ def insert_hyphens(node, hyphenator): for child in node.iterchildren(): insert_hyphens(child, hyphenator) - - -CRYPT = string.Template("""\ -<script> -function rc4(key, str) { - var s = [], j = 0, x, res = ''; - for (var i = 0; i < 256; i++) { - s[i] = i; - } - for (i = 0; i < 256; i++) { - j = (j + s[i] + key.charCodeAt(i % key.length)) % 256; - x = s[i]; - s[i] = s[j]; - s[j] = x; - } - i = 0; - j = 0; - for (var y = 0; y < str.length; y++) { - i = (i + 1) % 256; - j = (j + s[i]) % 256; - x = s[i]; - s[i] = s[j]; - s[j] = x; - res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]); - } - return res; -} -function decrypt() { - key = $$("#key").val(); - crypt_div = $$("#encr") - crypted = crypt_div.html(); - decrypted = rc4(key, window.atob(crypted)); - if (decrypted.substr(decrypted.length - 11) == "<!--tail-->"){ - crypt_div.html(decrypted); - $$("#pwform").hide(); - crypt_div.show(); - } else { alert("Wrong password"); }; -} -</script> - -<div id="encr" style="display: none;">${data}</div> -<div id="pwform"> -<form onsubmit="javascript:decrypt(); return false;" class="form-inline"> -<fieldset> -<legend>This post is password-protected.</legend> -<input type="password" id="key" placeholder="Type password here"> -<button type="submit" class="btn">Show Content</button> -</fieldset> -</form> -</div>""") diff --git a/nikola/rc4.py b/nikola/rc4.py deleted file mode 100644 index 93b660f..0000000 --- a/nikola/rc4.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -""" -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 - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. -""" - -import base64 -import sys - - -def KSA(key): - """Run Key Scheduling Algorithm.""" - keylength = len(key) - - S = list(range(256)) - - j = 0 - for i in range(256): - j = (j + S[i] + key[i % keylength]) % 256 - S[i], S[j] = S[j], S[i] # swap - - return S - - -def PRGA(S): - """Run Pseudo-Random Generation Algorithm.""" - i = 0 - j = 0 - while True: - i = (i + 1) % 256 - j = (j + S[i]) % 256 - S[i], S[j] = S[j], S[i] # swap - - K = S[(S[i] + S[j]) % 256] - yield K - - -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') - - def convert_key(s): - return [ord(c) for c in s] - key = convert_key(key) - keystream = RC4(key) - r = b'' - for c in string: - if sys.version_info[0] == 3: - r += bytes([ord(c) ^ next(keystream)]) - else: - r += chr(ord(c) ^ next(keystream)) - return base64.b64encode(r).replace(b'\n', b'').decode('ascii') diff --git a/nikola/shortcodes.py b/nikola/shortcodes.py index b11ddac..6116b98 100644 --- a/nikola/shortcodes.py +++ b/nikola/shortcodes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,10 @@ """Support for Hugo-style shortcodes.""" -from __future__ import unicode_literals -from .utils import LOGGER import sys +import uuid - -# Constants -_TEXT = 1 -_SHORTCODE_START = 2 -_SHORTCODE_END = 3 +from .utils import LOGGER class ParsingError(Exception): @@ -83,11 +78,10 @@ def _skip_whitespace(data, pos, must_be_nontrivial=False): def _skip_nonwhitespace(data, pos): """Return first position not before pos which contains a non-whitespace character.""" - while pos < len(data): - if data[pos].isspace(): - break - pos += 1 - return pos + for i, x in enumerate(data[pos:]): + if x.isspace(): + return pos + i + return len(data) def _parse_quoted_string(data, start): @@ -209,14 +203,69 @@ def _parse_shortcode_args(data, start, shortcode_name, start_pos): raise ParsingError("Shortcode '{0}' starting at {1} is not terminated correctly with '%}}}}'!".format(shortcode_name, _format_position(data, start_pos))) +def _new_sc_id(): + return str('SHORTCODE{0}REPLACEMENT'.format(str(uuid.uuid4()).replace('-', ''))) + + +def extract_shortcodes(data): + """ + Return data with replaced shortcodes, shortcodes. + + data is the original data, with the shortcodes replaced by UUIDs. + + a dictionary of shortcodes, where the keys are UUIDs and the values + are the shortcodes themselves ready to process. + """ + shortcodes = {} + splitted = _split_shortcodes(data) + + if not data: # Empty + return '', {} + + def extract_data_chunk(data): + """Take a list of splitted shortcodes and return a string and a tail. + + The string is data, the tail is ready for a new run of this same function. + """ + text = [] + for i, token in enumerate(data): + if token[0] == 'SHORTCODE_START': + name = token[3] + sc_id = _new_sc_id() + text.append(sc_id) + # See if this shortcode closes + for j in range(i, len(data)): + if data[j][0] == 'SHORTCODE_END' and data[j][3] == name: + # Extract this chunk + shortcodes[sc_id] = ''.join(t[1] for t in data[i:j + 1]) + return ''.join(text), data[j + 1:] + # Doesn't close + shortcodes[sc_id] = token[1] + return ''.join(text), data[i + 1:] + elif token[0] == 'TEXT': + text.append(token[1]) + return ''.join(text), data[1:] + elif token[0] == 'SHORTCODE_END': # This is malformed + raise Exception('Closing unopened shortcode {}'.format(token[3])) + + text = [] + tail = splitted + while True: + new_text, tail = extract_data_chunk(tail) + text.append(new_text) + if not tail: + break + return ''.join(text), shortcodes + + def _split_shortcodes(data): """Given input data, splits it into a sequence of texts, shortcode starts and shortcode ends. Returns a list of tuples of the following forms: - 1. (_TEXT, text) - 2. (_SHORTCODE_START, text, start, name, args) - 3. (_SHORTCODE_END, text, start, name) + 1. ("TEXT", text) + 2. ("SHORTCODE_START", text, start, name, args) + 3. ("SHORTCODE_END", text, start, name) Here, text is the raw text represented by the token; start is the starting position in data of the token; name is the name of the shortcode; and args is a tuple (args, kw) as returned @@ -228,9 +277,9 @@ def _split_shortcodes(data): # Search for shortcode start start = data.find('{{%', pos) if start < 0: - result.append((_TEXT, data[pos:])) + result.append(("TEXT", data[pos:])) break - result.append((_TEXT, data[pos:start])) + result.append(("TEXT", data[pos:start])) # Extract name name_start = _skip_whitespace(data, start + 3) name_end = _skip_nonwhitespace(data, name_start) @@ -246,18 +295,17 @@ def _split_shortcodes(data): # Must be followed by '%}}' if pos > len(data) or data[end_start:pos] != '%}}': raise ParsingError("Syntax error: '{{{{% /{0}' must be followed by ' %}}}}' ({1})!".format(name, _format_position(data, end_start))) - result.append((_SHORTCODE_END, data[start:pos], start, name)) + result.append(("SHORTCODE_END", data[start:pos], start, name)) elif name == '%}}': raise ParsingError("Syntax error: '{{{{%' must be followed by shortcode name ({0})!".format(_format_position(data, start))) else: # This is an opening shortcode pos, args = _parse_shortcode_args(data, name_end, shortcode_name=name, start_pos=start) - result.append((_SHORTCODE_START, data[start:pos], start, name, args)) + result.append(("SHORTCODE_START", data[start:pos], start, name, args)) return result -# FIXME: in v8, get rid of with_dependencies -def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=False, lang=None, with_dependencies=False, extra_context={}): +def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=False, lang=None, extra_context=None): """Apply Hugo-style shortcodes on data. {{% name parameters %}} will end up calling the registered "name" function with the given parameters. @@ -274,7 +322,9 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions= >>> print(apply_shortcodes('==> {{% foo bar=baz %}}some data{{% /foo %}} <==', {'foo': lambda *a, **k: k['bar']+k['data']})) ==> bazsome data <== """ - empty_string = data[:0] # same string type as data; to make Python 2 happy + if extra_context is None: + extra_context = {} + empty_string = '' try: # Split input data into text, shortcodes and shortcode endings sc_data = _split_shortcodes(data) @@ -284,17 +334,17 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions= pos = 0 while pos < len(sc_data): current = sc_data[pos] - if current[0] == _TEXT: + if current[0] == "TEXT": result.append(current[1]) pos += 1 - elif current[0] == _SHORTCODE_END: + elif current[0] == "SHORTCODE_END": raise ParsingError("Found shortcode ending '{{{{% /{0} %}}}}' which isn't closing a started shortcode ({1})!".format(current[3], _format_position(data, current[2]))) - elif current[0] == _SHORTCODE_START: + elif current[0] == "SHORTCODE_START": name = current[3] # Check if we can find corresponding ending found = None for p in range(pos + 1, len(sc_data)): - if sc_data[p][0] == _SHORTCODE_END and sc_data[p][3] == name: + if sc_data[p][0] == "SHORTCODE_END" and sc_data[p][3] == name: found = p break if found: @@ -321,17 +371,15 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions= if not isinstance(res, tuple): # For backards compatibility res = (res, []) else: - LOGGER.error('Unknown shortcode {0} (started at {1})', name, _format_position(data, current[2])) + LOGGER.error('Unknown shortcode %s (started at %s)', name, _format_position(data, current[2])) res = ('', []) result.append(res[0]) dependencies += res[1] - if with_dependencies: - return empty_string.join(result), dependencies - return empty_string.join(result) + return empty_string.join(result), dependencies except ParsingError as e: if raise_exceptions: # Throw up - raise e + raise if filename: LOGGER.error("Shortcode error in file {0}: {1}".format(filename, e)) else: diff --git a/nikola/state.py b/nikola/state.py index 6632e4f..4669d13 100644 --- a/nikola/state.py +++ b/nikola/state.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -81,12 +81,7 @@ class Persistor(): def _save(self): dname = os.path.dirname(self._path) - with tempfile.NamedTemporaryFile(dir=dname, delete=False) as outf: - # TODO replace with encoding='utf-8' and mode 'w+' in v8 + with tempfile.NamedTemporaryFile(dir=dname, delete=False, mode='w+', encoding='utf-8') as outf: tname = outf.name - data = json.dumps(self._local.data, sort_keys=True, indent=2) - try: - outf.write(data) - except TypeError: - outf.write(data.encode('utf-8')) + json.dump(self._local.data, outf, sort_keys=True, indent=2) shutil.move(tname, self._path) diff --git a/nikola/utils.py b/nikola/utils.py index 068cb3a..d029b7f 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,15 +26,11 @@ """Utility functions.""" -from __future__ import print_function, unicode_literals, absolute_import -import calendar +import configparser import datetime -import dateutil.tz import hashlib import io -import locale -import logging -import natsort +import operator import os import re import json @@ -42,131 +38,87 @@ import shutil import socket import subprocess import sys +import threading +import typing +from collections import defaultdict, OrderedDict +from collections.abc import Callable, Iterable +from html import unescape as html_unescape +from importlib import reload as _reload +from unicodedata import normalize as unicodenormalize +from urllib.parse import quote as urlquote +from urllib.parse import unquote as urlunquote +from urllib.parse import urlparse, urlunparse +from zipfile import ZipFile as zipf + +import babel.dates import dateutil.parser import dateutil.tz -import logbook -try: - from urllib import quote as urlquote - from urllib import unquote as urlunquote - from urlparse import urlparse, urlunparse -except ImportError: - from urllib.parse import quote as urlquote # NOQA - from urllib.parse import unquote as urlunquote # NOQA - from urllib.parse import urlparse, urlunparse # NOQA -import warnings +import pygments.formatters +import pygments.formatters._mapping import PyRSS2Gen as rss +from blinker import signal +from doit import tools +from doit.cmdparse import CmdParse +from pkg_resources import resource_filename +from nikola.packages.pygments_better_html import BetterHtmlFormatter +from unidecode import unidecode + +# Renames +from nikola import DEBUG # NOQA +from .log import LOGGER, get_logger # NOQA +from .hierarchy_utils import TreeNode, clone_treenode, flatten_tree_structure, sort_classifications +from .hierarchy_utils import join_hierarchical_category_path, parse_escaped_hierarchical_category_name + try: - import pytoml as toml + import toml except ImportError: toml = None + try: - import yaml + from ruamel.yaml import YAML except ImportError: - yaml = None + YAML = None + try: import husl except ImportError: husl = None -from collections import defaultdict, Callable, OrderedDict -from logbook.compat import redirect_logging -from logbook.more import ExceptionHandler, ColorizedStderrHandler -from pygments.formatters import HtmlFormatter -from zipfile import ZipFile as zipf -from doit import tools -from unidecode import unidecode -from unicodedata import normalize as unicodenormalize -from pkg_resources import resource_filename -from doit.cmdparse import CmdParse - -from nikola import DEBUG - -__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real', 'get_theme_chain', 'load_messages', 'copy_tree', - 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters', +__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real', + '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', - 'TranslatableSetting', 'TemplateHookRegistry', 'LocaleBorg', + '_reload', 'Functionary', 'TranslatableSetting', + 'TemplateHookRegistry', 'LocaleBorg', 'sys_encode', 'sys_decode', 'makedirs', 'get_parent_theme_name', 'demote_headers', 'get_translation_candidate', 'write_metadata', 'ask', 'ask_yesno', 'options2docstring', 'os_path_split', 'get_displayed_page_number', 'adjust_name_for_index_path_list', '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', 'clean_before_deployment', 'indent', - 'load_data') + 'NikolaPygmentsHTML', 'create_redirect', 'clean_before_deployment', + 'sort_posts', 'smartjoin', 'indent', 'load_data', 'html_unescape', + 'rss_writer', 'map_metadata', 'req_missing', + # Deprecated, moved to hierarchy_utils: + 'TreeNode', 'clone_treenode', 'flatten_tree_structure', + 'sort_classifications', 'join_hierarchical_category_path', + 'parse_escaped_hierarchical_category_name',) # Are you looking for 'generic_rss_renderer'? # It's defined in nikola.nikola.Nikola (the site object). -if sys.version_info[0] == 3: - # Python 3 - bytes_str = bytes - unicode_str = str - unichr = chr - raw_input = input - from imp import reload as _reload -else: - bytes_str = str - unicode_str = unicode # NOQA - _reload = reload # NOQA - unichr = unichr - - -class ApplicationWarning(Exception): - pass - - -class ColorfulStderrHandler(ColorizedStderrHandler): - """Stream handler with colors.""" +# Aliases, previously for Python 2/3 compatibility. +# TODO remove in v9 +bytes_str = bytes +unicode_str = str +unichr = chr - _colorful = False +# For compatibility with old logging setups. +# TODO remove in v9? +STDERR_HANDLER = None - def should_colorize(self, record): - """Inform about colorization using the value obtained from Nikola.""" - return self._colorful - - -def get_logger(name, handlers): - """Get a logger with handlers attached.""" - l = logbook.Logger(name) - for h in handlers: - if isinstance(h, list): - l.handlers += h - else: - l.handlers.append(h) - return l - - -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 -redirect_logging() - -if DEBUG: - logging.basicConfig(level=logging.DEBUG) -else: - logging.basicConfig(level=logging.INFO) - - -def showwarning(message, category, filename, lineno, file=None, line=None): - """Show a warning (from the warnings module) to the user.""" - try: - n = category.__name__ - except AttributeError: - n = str(category) - get_logger(n, STDERR_HANDLER).warn('{0}:{1}: {2}'.format(filename, lineno, message)) - -warnings.showwarning = showwarning - def req_missing(names, purpose, python=True, optional=False): """Log that we are missing some requirements. @@ -205,7 +157,7 @@ def req_missing(names, purpose, python=True, optional=False): purpose, pnames, whatarethey_p) if optional: - LOGGER.warn(msg) + LOGGER.warning(msg) else: LOGGER.error(msg) LOGGER.error('Exiting due to missing dependencies.') @@ -214,20 +166,19 @@ def req_missing(names, purpose, python=True, optional=False): return msg -from nikola import filters as task_filters # NOQA ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding def sys_encode(thing): """Return bytes encoded in the system's encoding.""" - if isinstance(thing, unicode_str): + if isinstance(thing, str): return thing.encode(ENCODING) return thing def sys_decode(thing): """Return Unicode.""" - if isinstance(thing, bytes_str): + if isinstance(thing, bytes): return thing.decode(ENCODING) return thing @@ -255,7 +206,7 @@ class Functionary(defaultdict): def __init__(self, default, default_lang): """Initialize a functionary.""" - super(Functionary, self).__init__(default) + super().__init__(default) self.default_lang = default_lang def __call__(self, key, lang=None): @@ -292,7 +243,7 @@ class TranslatableSetting(object): def __getattribute__(self, attr): """Return attributes, falling back to string attributes.""" try: - return super(TranslatableSetting, self).__getattribute__(attr) + return super().__getattribute__(attr) except AttributeError: return self().__getattribute__(attr) @@ -356,15 +307,11 @@ class TranslatableSetting(object): def __str__(self): """Return the value in the currently set language (deprecated).""" - return self.values[self.get_lang()] - - def __unicode__(self): - """Return the value in the currently set language (deprecated).""" - return self.values[self.get_lang()] + return str(self.values[self.get_lang()]) def __repr__(self): """Provide a representation for programmers.""" - return '<TranslatableSetting: {0!r}>'.format(self.name) + return '<TranslatableSetting: {0!r} = {1!r}>'.format(self.name, self._inp) def format(self, *args, **kwargs): """Format ALL the values in the setting the same way.""" @@ -465,9 +412,8 @@ class TemplateHookRegistry(object): >>> r = TemplateHookRegistry('foo', None) >>> r.append('Hello!') >>> r.append(lambda x: 'Hello ' + x + '!', False, 'world') - >>> str(r()) # str() call is not recommended in real use + >>> repr(r()) 'Hello!\nHello world!' - >>> """ def __init__(self, name, site): @@ -509,9 +455,23 @@ class TemplateHookRegistry(object): c = callable(inp) self._items.append((c, inp, wants_site_and_context, args, kwargs)) + def calculate_deps(self): + """Calculate dependencies for a registry.""" + deps = [] + for is_callable, inp, wants_site_and_context, args, kwargs in self._items: + if not is_callable: + name = inp + elif hasattr(inp, 'template_registry_identifier'): + name = inp.template_registry_identifier + elif hasattr(inp, '__doc__'): + name = inp.__doc__ + else: + name = '_undefined_callable_' + deps.append((is_callable, name, wants_site_and_context, args, kwargs)) + def __hash__(self): """Return hash of a registry.""" - return hash(config_changed({self.name: self._items})._calc_digest()) + return hash(config_changed({self.name: self.calculate_deps()})._calc_digest()) def __str__(self): """Stringify a registry.""" @@ -526,12 +486,14 @@ class CustomEncoder(json.JSONEncoder): """Custom JSON encoder.""" def default(self, obj): - """Default encoding handler.""" + """Create default encoding handler.""" try: - return super(CustomEncoder, self).default(obj) + return super().default(obj) except TypeError: if isinstance(obj, (set, frozenset)): return self.encode(sorted(list(obj))) + elif isinstance(obj, TranslatableSetting): + s = json.dumps(obj._inp, cls=CustomEncoder, sort_keys=True) else: s = repr(obj).split('0x', 1)[0] return s @@ -542,11 +504,34 @@ class config_changed(tools.config_changed): def __init__(self, config, identifier=None): """Initialize config_changed.""" - super(config_changed, self).__init__(config) + super().__init__(config) self.identifier = '_config_changed' if identifier is not None: self.identifier += ':' + identifier + # DEBUG (for unexpected rebuilds) + @classmethod + def _write_into_debug_db(cls, digest: str, data: str) -> None: # pragma: no cover + """Write full values of config_changed into a sqlite3 database.""" + import sqlite3 + try: + cls.debug_db_cursor + except AttributeError: + cls.debug_db_conn = sqlite3.connect("cc_debug.sqlite3") + cls.debug_db_id = datetime.datetime.now().isoformat() + cls.debug_db_cursor = cls.debug_db_conn.cursor() + cls.debug_db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS hashes (hash CHARACTER(32) PRIMARY KEY, json_data TEXT); + """) + cls.debug_db_conn.commit() + + try: + cls.debug_db_cursor.execute("INSERT INTO hashes (hash, json_data) VALUES (?, ?);", (digest, data)) + cls.debug_db_conn.commit() + except sqlite3.IntegrityError: + # ON CONFLICT DO NOTHING, except Ubuntu 16.04’s sqlite3 is too ancient for this + cls.debug_db_conn.rollback() + def _calc_digest(self): """Calculate a config_changed digest.""" if isinstance(self.config, str): @@ -558,9 +543,14 @@ class config_changed(tools.config_changed): else: byte_data = data digest = hashlib.md5(byte_data).hexdigest() + + # DEBUG (for unexpected rebuilds) + # self._write_into_debug_db(digest, data) + # Alternative (without database): # LOGGER.debug('{{"{0}": {1}}}'.format(digest, byte_data)) # Humanized format: # LOGGER.debug('[Digest {0} for {2}]\n{1}\n[Digest {0} for {2}]'.format(digest, byte_data, self.identifier)) + return digest else: raise Exception('Invalid type of config_changed parameter -- got ' @@ -605,27 +595,52 @@ def get_theme_path(theme): return theme +def parse_theme_meta(theme_dir): + """Parse a .theme meta file.""" + cp = configparser.ConfigParser() + # The `or` case is in case theme_dir ends with a trailing slash + theme_name = os.path.basename(theme_dir) or os.path.basename(os.path.dirname(theme_dir)) + theme_meta_path = os.path.join(theme_dir, theme_name + '.theme') + cp.read(theme_meta_path) + return cp if cp.has_section('Theme') else None + + def get_template_engine(themes): """Get template engine used by a given theme.""" for theme_name in themes: - engine_path = os.path.join(theme_name, 'engine') - if os.path.isfile(engine_path): - with open(engine_path) as fd: - return fd.readlines()[0].strip() + meta = parse_theme_meta(theme_name) + if meta: + e = meta.get('Theme', 'engine', fallback=None) + if e: + return e + else: + # Theme still uses old-style parent/engine files + engine_path = os.path.join(theme_name, 'engine') + if os.path.isfile(engine_path): + with open(engine_path) as fd: + return fd.readlines()[0].strip() # default return 'mako' def get_parent_theme_name(theme_name, themes_dirs=None): """Get name of parent theme.""" - parent_path = os.path.join(theme_name, 'parent') - if os.path.isfile(parent_path): - with open(parent_path) as fd: - parent = fd.readlines()[0].strip() - if themes_dirs: + meta = parse_theme_meta(theme_name) + if meta: + parent = meta.get('Theme', 'parent', fallback=None) + if themes_dirs and parent: return get_theme_path_real(parent, themes_dirs) return parent - return None + else: + # Theme still uses old-style parent/engine files + parent_path = os.path.join(theme_name, 'parent') + if os.path.isfile(parent_path): + with open(parent_path) as fd: + parent = fd.readlines()[0].strip() + if themes_dirs: + return get_theme_path_real(parent, themes_dirs) + return parent + return None def get_theme_chain(theme, themes_dirs): @@ -641,7 +656,7 @@ def get_theme_chain(theme, themes_dirs): return themes -language_incomplete_warned = [] +INCOMPLETE_LANGUAGES_WARNED = set() class LanguageNotFoundError(Exception): @@ -665,38 +680,50 @@ def load_messages(themes, translations, default_lang, themes_dirs): """ messages = Functionary(dict, default_lang) oldpath = list(sys.path) + found = {lang: False for lang in translations.keys()} + last_exception = None + completion_status = {lang: False for lang in translations.keys()} for theme_name in themes[::-1]: msg_folder = os.path.join(get_theme_path(theme_name), 'messages') default_folder = os.path.join(get_theme_path_real('base', themes_dirs), 'messages') sys.path.insert(0, default_folder) sys.path.insert(0, msg_folder) + english = __import__('messages_en') # If we don't do the reload, the module is cached _reload(english) - for lang in list(translations.keys()): + for lang in translations.keys(): try: translation = __import__('messages_' + lang) # If we don't do the reload, the module is cached _reload(translation) - if sorted(translation.MESSAGES.keys()) !=\ - sorted(english.MESSAGES.keys()) and \ - lang not in language_incomplete_warned: - language_incomplete_warned.append(lang) - LOGGER.warn("Incomplete translation for language " - "'{0}'.".format(lang)) + found[lang] = True + if sorted(translation.MESSAGES.keys()) != sorted(english.MESSAGES.keys()): + completion_status[lang] = completion_status[lang] or False + else: + completion_status[lang] = True + messages[lang].update(english.MESSAGES) for k, v in translation.MESSAGES.items(): if v: messages[lang][k] = v del(translation) except ImportError as orig: - raise LanguageNotFoundError(lang, orig) + last_exception = orig del(english) - sys.path = oldpath + sys.path = oldpath + + if not all(found.values()): + raise LanguageNotFoundError(lang, last_exception) + for lang, status in completion_status.items(): + if not status and lang not in INCOMPLETE_LANGUAGES_WARNED: + LOGGER.warning("Incomplete translation for language '{0}'.".format(lang)) + INCOMPLETE_LANGUAGES_WARNED.add(lang) + return messages -def copy_tree(src, dst, link_cutoff=None): +def copy_tree(src, dst, link_cutoff=None, ignored_filenames=None): """Copy a src tree to the dst folder. Example: @@ -707,11 +734,13 @@ def copy_tree(src, dst, link_cutoff=None): should copy "themes/defauts/assets/foo/bar" to "output/assets/foo/bar" - if link_cutoff is set, then the links pointing at things + If link_cutoff is set, then the links pointing at things *inside* that folder will stay as links, and links pointing *outside* that folder will be copied. + + ignored_filenames is a set of file names that will be ignored. """ - ignore = set(['.svn']) + ignore = set(['.svn', '.git']) | (ignored_filenames or set()) base_len = len(src.split(os.sep)) for root, dirs, files in os.walk(src, followlinks=True): root_parts = root.split(os.sep) @@ -761,11 +790,12 @@ def remove_file(source): elif os.path.isfile(source) or os.path.islink(source): os.remove(source) + # slugify is adopted from # http://code.activestate.com/recipes/ # 577257-slugify-make-a-string-usable-in-a-url-or-filename/ -_slugify_strip_re = re.compile(r'[^+\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') +_slugify_strip_re = re.compile(r'[^+\w\s-]', re.UNICODE) +_slugify_hyphenate_re = re.compile(r'[-\s]+', re.UNICODE) def slugify(value, lang=None, force=False): @@ -782,16 +812,14 @@ def slugify(value, lang=None, force=False): >>> print(slugify('foo bar', lang='en')) foo-bar """ - if lang is None: # TODO: remove in v8 - LOGGER.warn("slugify() called without language!") - if not isinstance(value, unicode_str): + if not isinstance(value, 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 = unicode_str(unidecode(value)) - value = _slugify_strip_re.sub('', value, re.UNICODE).strip().lower() - return _slugify_hyphenate_re.sub('-', value, re.UNICODE) + value = str(unidecode(value)) + value = _slugify_strip_re.sub('', value).strip().lower() + return _slugify_hyphenate_re.sub('-', value) else: # This is the “disarmed” state of slugify, which lets the user # have any character they please (be it regular ASCII with spaces, @@ -814,11 +842,9 @@ def unslugify(value, lang=None, discard_numbers=True): If discard_numbers is True, numbers right at the beginning of input will be removed. """ - if lang is None: # TODO: remove in v8 - LOGGER.warn("unslugify() called without language!") if discard_numbers: value = re.sub('^[0-9]+', '', value) - value = re.sub('([_\-\.])', ' ', value) + value = re.sub(r'([_\-\.])', ' ', value) value = value.strip().capitalize() return value @@ -835,6 +861,16 @@ def encodelink(iri): encoded_link = urlunparse(link.values()) return encoded_link + +def full_path_from_urlparse(parsed) -> str: + """Given urlparse output, return the full path (with query and fragment).""" + dst = parsed.path + if parsed.query: + dst = "{0}?{1}".format(dst, parsed.query) + if parsed.fragment: + dst = "{0}#{1}".format(dst, parsed.fragment) + return dst + # A very slightly safer version of zip.extractall that works on # python < 2.6 @@ -868,6 +904,8 @@ def extract_all(zipfile, path='themes'): def to_datetime(value, tzinfo=None): """Convert string to datetime.""" try: + if type(value) == datetime.date: + value = datetime.datetime.combine(value, datetime.time(0, 0)) if not isinstance(value, datetime.datetime): # dateutil does bad things with TZs like UTC-03:00. dateregexp = re.compile(r' UTC([+-][0-9][0-9]:[0-9][0-9])') @@ -898,6 +936,9 @@ def current_time(tzinfo=None): return dt +from nikola import filters as task_filters # NOQA + + def apply_filters(task, filters, skip_ext=None): """Apply filters to a task. @@ -916,11 +957,11 @@ def apply_filters(task, filters, skip_ext=None): if isinstance(key, (tuple, list)): if ext in key: return value - elif isinstance(key, (bytes_str, unicode_str)): + elif isinstance(key, (bytes, str)): if ext == key: return value else: - assert False, key + raise ValueError("Cannot find filter match for {0}".format(key)) for target in task.get('targets', []): ext = os.path.splitext(target)[-1].lower() @@ -949,26 +990,26 @@ def get_crumbs(path, is_file=False, index_folder=None, lang=None): >>> crumbs = get_crumbs('galleries') >>> len(crumbs) 1 - >>> print('|'.join(crumbs[0])) - #|galleries + >>> crumbs[0] + ['#', 'galleries'] >>> crumbs = get_crumbs(os.path.join('galleries','demo')) >>> len(crumbs) 2 - >>> print('|'.join(crumbs[0])) - ..|galleries - >>> print('|'.join(crumbs[1])) - #|demo + >>> crumbs[0] + ['..', 'galleries'] + >>> crumbs[1] + ['#', 'demo'] >>> crumbs = get_crumbs(os.path.join('listings','foo','bar'), is_file=True) >>> len(crumbs) 3 - >>> print('|'.join(crumbs[0])) - ..|listings - >>> print('|'.join(crumbs[1])) - .|foo - >>> print('|'.join(crumbs[2])) - #|bar + >>> crumbs[0] + ['..', 'listings'] + >>> crumbs[1] + ['.', 'foo'] + >>> crumbs[2] + ['#', 'bar'] """ crumbs = path.split(os.sep) _crumbs = [] @@ -1009,8 +1050,8 @@ def get_asset_path(path, themes, files_folders={'files': ''}, output_dir='output If it's not provided by either, it will be chacked in output, where it may have been created by another plugin. - >>> print(get_asset_path('assets/css/rst.css', get_theme_chain('bootstrap3', ['themes']))) - /.../nikola/data/themes/base/assets/css/rst.css + >>> print(get_asset_path('assets/css/nikola_rst.css', get_theme_chain('bootstrap3', ['themes']))) + /.../nikola/data/themes/base/assets/css/nikola_rst.css >>> print(get_asset_path('assets/css/theme.css', get_theme_chain('bootstrap3', ['themes']))) /.../nikola/data/themes/bootstrap3/assets/css/theme.css @@ -1050,24 +1091,49 @@ class LocaleBorgUninitializedException(Exception): def __init__(self): """Initialize exception.""" - super(LocaleBorgUninitializedException, self).__init__("Attempt to use LocaleBorg before initialization") + super().__init__("Attempt to use LocaleBorg before initialization") + + +# Customized versions of babel.dates functions that don't do weird stuff with +# timezones. Without these fixes, DST would follow local settings (because +# dateutil’s timezones return stuff depending on their input, and datetime.time +# objects have no year/month/day to base the information on. +def format_datetime(datetime=None, format='medium', + locale=babel.dates.LC_TIME): + """Format a datetime object.""" + locale = babel.dates.Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + return babel.dates.get_datetime_format(format, locale=locale) \ + .replace("'", "") \ + .replace('{0}', format_time(datetime, format, locale=locale)) \ + .replace('{1}', babel.dates.format_date(datetime, format, locale=locale)) + else: + return babel.dates.parse_pattern(format).apply(datetime, locale) -class LocaleBorg(object): - """Provide locale related services and autoritative current_lang. +def format_time(time=None, format='medium', locale=babel.dates.LC_TIME): + """Format time. Input can be datetime.time or datetime.datetime.""" + locale = babel.dates.Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + format = babel.dates.get_time_format(format, locale=locale) + return babel.dates.parse_pattern(format).apply(time, locale) - 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 - better handled at one central point, LocaleBorg. +def format_skeleton(skeleton, datetime=None, fo=None, fuzzy=True, + locale=babel.dates.LC_TIME): + """Format a datetime based on a skeleton.""" + locale = babel.dates.Locale.parse(locale) + if fuzzy and skeleton not in locale.datetime_skeletons: + skeleton = babel.dates.match_skeleton(skeleton, locale.datetime_skeletons) + format = locale.datetime_skeletons[skeleton] + return format_datetime(datetime, format, locale) - In particular, don't call locale.setlocale outside of LocaleBorg. - Assumptions: - We need locales only for the languages there is a nikola translation. - We don't need to support current_lang through nested contexts +class LocaleBorg(object): + """Provide locale related services and autoritative current_lang. + + This class stores information about the locales used and interfaces + with the Babel library to provide internationalization services. Usage: # early in cmd or test execution @@ -1077,46 +1143,39 @@ class LocaleBorg(object): lang = LocaleBorg().<service> Available services: - .current_lang : autoritative current_lang , the last seen in set_locale - .set_locale(lang) : sets current_lang and sets the locale for lang - .get_month_name(month_no, lang) : returns the localized month name + .current_lang: autoritative current_lang, the last seen in set_locale + .formatted_date: format a date(time) according to locale rules + .format_date_in_string: take a message and format the date in it - NOTE: never use locale.getlocale() , it can return values that - locale.setlocale will not accept in Windows XP, 7 and pythons 2.6, 2.7, 3.3 - Examples: "Spanish", "French" can't do the full circle set / get / set + The default implementation uses the Babel package and completely ignores + the Python `locale` module. If you wish to override this, write functions + and assign them to the appropriate names. The functions are: + + * LocaleBorg.datetime_formatter(date, date_format, lang, locale) + * LocaleBorg.in_string_formatter(date, mode, custom_format, lang, locale) """ initialized = False + # Can be used to override Babel + datetime_formatter = None + in_string_formatter = None + @classmethod - def initialize(cls, locales, initial_lang): + def initialize(cls, locales: 'typing.Dict[str, str]', initial_lang: str): """Initialize LocaleBorg. - locales : dict with lang: locale_n - the same keys as in nikola's TRANSLATIONS - locale_n a sanitized locale, meaning - locale.setlocale(locale.LC_ALL, locale_n) will succeed - locale_n expressed in the string form, like "en.utf8" + locales: dict with custom locale name overrides. """ - assert initial_lang is not None and initial_lang in locales + if not initial_lang: + raise ValueError("Unknown initial language {0}".format(initial_lang)) cls.reset() cls.locales = locales - cls.month_name_handlers = [] - cls.formatted_date_handlers = [] - - # needed to decode some localized output in py2x - encodings = {} - for lang in locales: - locale.setlocale(locale.LC_ALL, locales[lang]) - loc, encoding = locale.getlocale() - encodings[lang] = encoding - - cls.encodings = encodings cls.__initial_lang = initial_lang cls.initialized = True def __get_shared_state(self): - if not self.initialized: + if not self.initialized: # pragma: no cover raise LocaleBorgUninitializedException() shared_state = getattr(self.__thread_local, 'shared_state', None) if shared_state is None: @@ -1130,38 +1189,14 @@ class LocaleBorg(object): Used in testing to prevent leaking state between tests. """ - import threading cls.__thread_local = threading.local() cls.__thread_lock = threading.Lock() cls.locales = {} - cls.encodings = {} cls.initialized = False - cls.month_name_handlers = [] - cls.formatted_date_handlers = [] cls.thread_local = None - cls.thread_lock = None - - @classmethod - def add_handler(cls, month_name_handler=None, formatted_date_handler=None): - """Allow to add month name and formatted date handlers. - - If month_name_handler is not None, it is expected to be a callable - which accepts (month_no, lang) and returns either a string or None. - - If formatted_date_handler is not None, it is expected to be a callable - which accepts (date_format, date, lang) and returns either a string or - None. - - A handler is expected to either return the correct result for the given - language and data, or return None to indicate it is not able to do the - job. In that case, the next handler is asked, and finally the default - implementation is used. - """ - if month_name_handler is not None: - cls.month_name_handlers.append(month_name_handler) - if formatted_date_handler is not None: - cls.formatted_date_handlers.append(formatted_date_handler) + cls.datetime_formatter = None + cls.in_string_formatter = None def __init__(self): """Initialize.""" @@ -1169,79 +1204,68 @@ class LocaleBorg(object): raise LocaleBorgUninitializedException() @property - def current_lang(self): + def current_lang(self) -> str: """Return the current language.""" return self.__get_shared_state()['current_lang'] - def __set_locale(self, lang): - """Set the locale for language lang without updating current_lang.""" - locale_n = self.locales[lang] - locale.setlocale(locale.LC_ALL, locale_n) - - def set_locale(self, lang): - """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. - In either case, the locale encoding is available in cls.encodings[lang] - """ + def set_locale(self, lang: str) -> str: + """Set the current language and return an empty string (to make use in templates easier).""" with self.__thread_lock: - # intentional non try-except: templates must ask locales with a lang, - # let the code explode here and not hide the point of failure - # Also, not guarded with an if lang==current_lang because calendar may - # put that out of sync - self.__set_locale(lang) self.__get_shared_state()['current_lang'] = lang return '' - def get_month_name(self, month_no, lang): - """Return localized month name in an unicode string.""" - # For thread-safety - with self.__thread_lock: - for handler in self.month_name_handlers: - res = handler(month_no, lang) - if res is not None: - return res - old_lang = self.current_lang - self.__set_locale(lang) - s = calendar.month_name[month_no] - self.__set_locale(old_lang) - if sys.version_info[0] == 2: - enc = self.encodings[lang] - if not enc: - enc = 'UTF-8' - - s = s.decode(enc) - return s + def formatted_date(self, date_format: 'str', + date: 'typing.Union[datetime.date, datetime.datetime]', + lang: 'typing.Optional[str]' = None) -> str: + """Return the formatted date/datetime as a string.""" + if lang is None: + lang = self.current_lang + locale = self.locales.get(lang, lang) + # Get a string out of a TranslatableSetting + if isinstance(date_format, TranslatableSetting): + date_format = date_format(lang) + + # Always ask Python if the date_format is webiso + if date_format == 'webiso': + # Formatted after RFC 3339 (web ISO 8501 profile) with Zulu + # zone designator for times in UTC and no microsecond precision. + return date.replace(microsecond=0).isoformat().replace('+00:00', 'Z') + elif LocaleBorg.datetime_formatter is not None: + return LocaleBorg.datetime_formatter(date, date_format, lang, locale) + else: + return format_datetime(date, date_format, locale=locale) - def formatted_date(self, date_format, date): - """Return the formatted date as unicode.""" - with self.__thread_lock: - current_lang = self.current_lang - # For thread-safety - self.__set_locale(current_lang) - fmt_date = None - # Get a string out of a TranslatableSetting - if isinstance(date_format, TranslatableSetting): - date_format = date_format(current_lang) - # First check handlers - for handler in self.formatted_date_handlers: - fmt_date = handler(date_format, date, current_lang) - if fmt_date is not None: - break - # If no handler was able to format the date, ask Python - if fmt_date is None: - 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') + def format_date_in_string(self, message: str, date: datetime.date, lang: 'typing.Optional[str]' = None) -> str: + """Format date inside a string (message). + + Accepted modes: month, month_year, month_day_year. + Format: {month} for standard, {month:MMMM} for customization. + """ + modes = { + 'month': ('date', 'LLLL'), + 'month_year': ('skeleton', 'yMMMM'), + 'month_day_year': ('date', 'long') + } + + if lang is None: + lang = self.current_lang + locale = self.locales.get(lang, lang) + + def date_formatter(match: typing.Match) -> str: + """Format a date as requested.""" + mode, custom_format = match.groups() + if LocaleBorg.in_string_formatter is not None: + return LocaleBorg.in_string_formatter(date, mode, custom_format, lang, locale) + elif custom_format: + return babel.dates.format_date(date, custom_format, locale) + else: + function, fmt = modes[mode] + if function == 'skeleton': + return format_skeleton(fmt, date, locale=locale) else: - fmt_date = date.strftime(date_format) + return babel.dates.format_date(date, fmt, locale) - # Issue #383, this changes from py2 to py3 - if isinstance(fmt_date, bytes_str): - fmt_date = fmt_date.decode('utf8') - return fmt_date + return re.sub(r'{(.*?)(?::(.*?))?}', date_formatter, message) class ExtendedRSS2(rss.RSS2): @@ -1253,8 +1277,7 @@ class ExtendedRSS2(rss.RSS2): """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) + super().publish(handler) def publish_extensions(self, handler): """Publish extensions.""" @@ -1272,9 +1295,10 @@ class ExtendedItem(rss.RSSItem): def __init__(self, **kw): """Initialize RSS item.""" - self.creator = kw.pop('creator') + self.creator = kw.pop('creator', None) + # It's an old style class - return rss.RSSItem.__init__(self, **kw) + rss.RSSItem.__init__(self, **kw) def publish_extensions(self, handler): """Publish extensions.""" @@ -1314,24 +1338,34 @@ def demote_headers(doc, level=1): if level == 0: return doc elif level > 0: - r = range(1, 7 - level) + levels = range(1, 7 - (level - 1)) + levels = reversed(levels) elif level < 0: - r = range(1 + level, 7) - for i in reversed(r): - # html headers go to 6, so we can’t “lower” beneath five - elements = doc.xpath('//h' + str(i)) - for e in elements: - e.tag = 'h' + str(i + level) + levels = range(2 + level, 7) + + for before in levels: + after = before + level + if after < 1: + # html headers can't go lower than 1 + after = 1 + elif after > 6: + # html headers go until 6 + after = 6 + + if before == after: + continue + + elements = doc.xpath('//h{}'.format(before)) + new_tag = 'h{}'.format(after) + for element in elements: + element.tag = new_tag def get_root_dir(): """Find root directory of nikola site by looking for conf.py.""" root = os.getcwd() - if sys.version_info[0] == 2: - confname = b'conf.py' - else: - confname = 'conf.py' + confname = 'conf.py' while True: if os.path.exists(os.path.join(root, confname)): @@ -1385,7 +1419,7 @@ def get_translation_candidate(config, path, lang): # This will still break if the user has ?*[]\ in the pattern. But WHY WOULD HE? pattern = pattern.replace('.', r'\.') pattern = pattern.replace('{path}', '(?P<path>.+?)') - pattern = pattern.replace('{ext}', '(?P<ext>[^\./]+)') + pattern = pattern.replace('{ext}', r'(?P<ext>[^\./]+)') pattern = pattern.replace('{lang}', '(?P<lang>{0})'.format('|'.join(config['TRANSLATIONS'].keys()))) m = re.match(pattern, path) if m and all(m.groups()): # It's a translated path @@ -1406,24 +1440,59 @@ def get_translation_candidate(config, path, lang): return config['TRANSLATIONS_PATTERN'].format(path=p, ext=e, lang=lang) -def write_metadata(data): - """Write metadata.""" - order = ('title', 'slug', 'date', 'tags', 'category', 'link', 'description', 'type') - f = '.. {0}: {1}' - meta = [] - for k in order: - try: - meta.append(f.format(k, data.pop(k))) - except KeyError: - pass +def write_metadata(data, metadata_format=None, comment_wrap=False, site=None, compiler=None): + """Write metadata. - # Leftover metadata (user-specified/non-default). - for k in natsort.natsorted(list(data.keys()), alg=natsort.ns.F | natsort.ns.IC): - meta.append(f.format(k, data[k])) - - meta.append('') - - return '\n'.join(meta) + Recommended usage: pass `site`, `comment_wrap` (True, False, or a 2-tuple of start/end markers), and optionally `compiler`. Other options are for backwards compatibility. + """ + # API compatibility + if metadata_format is None and site is not None: + metadata_format = site.config.get('METADATA_FORMAT', 'nikola').lower() + if metadata_format is None: + metadata_format = 'nikola' + + if site is None: + import nikola.metadata_extractors + metadata_extractors_by = nikola.metadata_extractors.default_metadata_extractors_by() + nikola.metadata_extractors.load_defaults(site, metadata_extractors_by) + else: + metadata_extractors_by = site.metadata_extractors_by + + # Pelican is mapped to rest_docinfo, markdown_meta, or nikola. + if metadata_format == 'pelican': + if compiler and compiler.name == 'rest': + metadata_format = 'rest_docinfo' + elif compiler and compiler.name == 'markdown': + metadata_format = 'markdown_meta' + else: + # Quiet fallback. + metadata_format = 'nikola' + + default_meta = ('nikola', 'rest_docinfo', 'markdown_meta') + extractor = metadata_extractors_by['name'].get(metadata_format) + if extractor and extractor.supports_write: + extractor.check_requirements() + return extractor.write_metadata(data, comment_wrap) + elif extractor and metadata_format not in default_meta: + LOGGER.warning('Writing METADATA_FORMAT {} is not supported, using "nikola" format'.format(metadata_format)) + elif metadata_format not in default_meta: + LOGGER.warning('Unknown METADATA_FORMAT {}, using "nikola" format'.format(metadata_format)) + + if metadata_format == 'rest_docinfo': + title = data['title'] + results = [ + '=' * len(title), + title, + '=' * len(title), + '' + ] + [':{0}: {1}'.format(k, v) for k, v in data.items() if v and k != 'title'] + [''] + return '\n'.join(results) + elif metadata_format == 'markdown_meta': + results = ['{0}: {1}'.format(k, v) for k, v in data.items() if v] + ['', ''] + return '\n'.join(results) + else: # Nikola, default + from nikola.metadata_extractors import DEFAULT_EXTRACTOR + return DEFAULT_EXTRACTOR.write_metadata(data, comment_wrap) def ask(query, default=None): @@ -1432,10 +1501,7 @@ def ask(query, default=None): default_q = ' [{0}]'.format(default) else: default_q = '' - if sys.version_info[0] == 3: - inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip() - else: - inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q).encode('utf-8')).strip() + inp = input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip() if inp or default is None: return inp else: @@ -1450,10 +1516,7 @@ def ask_yesno(query, default=None): default_q = ' [Y/n]' elif default is False: default_q = ' [y/N]' - if sys.version_info[0] == 3: - inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q)).strip() - else: - inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q).encode('utf-8')).strip() + inp = input("{query}{default_q} ".format(query=query, default_q=default_q)).strip() if inp: return inp.lower().startswith('y') elif default is not None: @@ -1502,10 +1565,6 @@ class Commands(object): # cleanup: run is doit-only, init is useless in an existing site if k in ['run', 'init']: continue - if sys.version_info[0] == 2: - k2 = bytes(k) - else: - k2 = k self._cmdnames.append(k) @@ -1516,7 +1575,7 @@ class Commands(object): # doit command: needs some help opt = v(config=self._config, **self._doitargs).get_options() nc = type( - k2, + k, (CommandWrapper,), { '__doc__': options2docstring(k, opt) @@ -1568,17 +1627,27 @@ def options2docstring(name, options): return '\n'.join(result) -class NikolaPygmentsHTML(HtmlFormatter): +class NikolaPygmentsHTML(BetterHtmlFormatter): """A Nikola-specific modification of Pygments' HtmlFormatter.""" - def __init__(self, anchor_ref, classes=None, linenos='table', linenostart=1): + def __init__(self, anchor_ref=None, classes=None, **kwargs): """Initialize formatter.""" if classes is None: classes = ['code', 'literal-block'] + if anchor_ref: + kwargs['lineanchors'] = slugify( + anchor_ref, lang=LocaleBorg().current_lang, force=True) self.nclasses = classes - super(NikolaPygmentsHTML, self).__init__( - cssclass='code', linenos=linenos, linenostart=linenostart, nowrap=False, - lineanchors=slugify(anchor_ref, lang=LocaleBorg().current_lang, force=True), anchorlinenos=True) + kwargs['cssclass'] = 'code' + if not kwargs.get('linenos'): + # Default to no line numbers (Issue #3426) + kwargs['linenos'] = False + if kwargs.get('linenos') not in {'table', 'inline', 'ol', False}: + # Map invalid values to table + kwargs['linenos'] = 'table' + kwargs['anchorlinenos'] = kwargs['linenos'] == 'table' + kwargs['nowrap'] = False + super().__init__(**kwargs) def wrap(self, source, outfile): """Wrap the ``source``, which is a generator yielding individual lines, in custom generators.""" @@ -1596,6 +1665,10 @@ class NikolaPygmentsHTML(HtmlFormatter): yield 0, '</pre>' +# For consistency, override the default formatter. +pygments.formatters._formatter_cache['HTML'] = NikolaPygmentsHTML + + def get_displayed_page_number(i, num_pages, site): """Get page number to be displayed for entry `i`.""" if not i: @@ -1621,7 +1694,7 @@ def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force path_list.append(index_file) if site.config["PRETTY_URLS"] and site.config["INDEXES_PRETTY_PAGE_URL"](lang) and path_list[-1] == index_file: path_schema = site.config["INDEXES_PRETTY_PAGE_URL"](lang) - if isinstance(path_schema, (bytes_str, unicode_str)): + if isinstance(path_schema, (bytes, str)): path_schema = [path_schema] else: path_schema = None @@ -1664,7 +1737,7 @@ def adjust_name_for_index_link(name, i, displayed_i, lang, site, force_addition= def create_redirect(src, dst): - """"Create a redirection.""" + """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' @@ -1674,139 +1747,6 @@ def create_redirect(src, dst): '<a href="{0}">here</a>.</p>\n</body>'.format(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 - - # The indent levels and changes allow to render a tree structure - # without keeping track of all that information during rendering. - # - # The indent_change_before is the different between the current - # comment's level and the previous comment's level; if the number - # is positive, the current level is indented further in, and if it - # is negative, it is indented further out. Positive values can be - # used to open HTML tags for each opened level. - # - # The indent_change_after is the difference between the next - # comment's level and the current comment's level. Negative values - # can be used to close HTML tags for each closed level. - # - # The indent_levels list contains one entry (index, count) per - # level, informing about the index of the current comment on that - # level and the count of comments on that level (before a comment - # of a higher level comes). This information can be used to render - # tree indicators, for example to generate a tree such as: - # - # +--- [(0,3)] - # +-+- [(1,3)] - # | +--- [(1,3), (0,2)] - # | +-+- [(1,3), (1,2)] - # | +--- [(1,3), (1,2), (0, 1)] - # +-+- [(2,3)] - # +- [(2,3), (0,1)] - # - # (The lists used as labels represent the content of the - # 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: - path.append(curr) - curr = curr.parent - 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): - for index, element in enumerate(input_list): - # add to destination - elements.append(element) - # compute and set indent levels - indent_levels = indent_levels_so_far + [(index, len(input_list))] - element.indent_levels = indent_levels - # add children - children = element.get_children() - element.children_count = len(children) - generate(children, indent_levels) - - generate(root_list, []) - # Add indent change counters - level = 0 - last_element = None - for element in elements: - new_level = len(element.indent_levels) - # Compute level change before this element - change = new_level - level - if last_element is not None: - last_element.indent_change_after = change - element.indent_change_before = change - # Update variables - level = new_level - last_element = element - # Set level change after last element - if last_element is not None: - last_element.indent_change_after = -level - return elements - - -def parse_escaped_hierarchical_category_name(category_name): - """Parse a category name.""" - result = [] - current = None - index = 0 - next_backslash = category_name.find('\\', index) - next_slash = category_name.find('/', index) - while index < len(category_name): - if next_backslash == -1 and next_slash == -1: - current = (current if current else "") + category_name[index:] - index = len(category_name) - elif next_slash >= 0 and (next_backslash == -1 or next_backslash > next_slash): - result.append((current if current else "") + category_name[index:next_slash]) - current = '' - index = next_slash + 1 - next_slash = category_name.find('/', index) - else: - if len(category_name) == next_backslash + 1: - raise Exception("Unexpected '\\' in '{0}' at last position!".format(category_name)) - esc_ch = category_name[next_backslash + 1] - if esc_ch not in {'/', '\\'}: - raise Exception("Unknown escape sequence '\\{0}' in '{1}'!".format(esc_ch, category_name)) - current = (current if current else "") + category_name[index:next_backslash] + esc_ch - index = next_backslash + 2 - next_backslash = category_name.find('\\', index) - if esc_ch == '/': - next_slash = category_name.find('/', index) - if current is not None: - result.append(current) - return result - - -def join_hierarchical_category_path(category_path): - """Join a category path.""" - def escape(s): - return s.replace('\\', '\\\\').replace('/', '\\/') - - return '/'.join([escape(p) for p in category_path]) - - def colorize_str_from_base_color(string, base_color): """Find a perceptual similar color from a base color based on the hash of a string. @@ -1815,14 +1755,7 @@ def colorize_str_from_base_color(string, base_color): lightness and saturation untouched using HUSL colorspace. """ def hash_str(string, pos): - x = hashlib.md5(string.encode('utf-8')).digest()[pos] - try: - # Python 2: a string - # TODO: remove in v8 - return ord(x) - except TypeError: - # Python 3: already an integer - return x + return hashlib.md5(string.encode('utf-8')).digest()[pos] def degreediff(dega, degb): return min(abs(dega - degb), abs((degb - dega) + 360)) @@ -1840,6 +1773,13 @@ def colorize_str_from_base_color(string, base_color): return husl.husl_to_hex(h, s, l) +def colorize_str(string: str, base_color: str, presets: dict): + """Colorize a string by using a presets dict or generate one based on base_color.""" + if string in presets: + return presets[string] + return colorize_str_from_base_color(string, base_color) + + def color_hsl_adjust_hex(hexstr, adjust_h=None, adjust_s=None, adjust_l=None): """Adjust a hex color using HSL arguments, adjustments in percentages 1.0 to -1.0. Returns a hex color.""" h, s, l = husl.hex_to_husl(hexstr) @@ -1901,6 +1841,64 @@ def clean_before_deployment(site): return undeployed_posts +def sort_posts(posts, *keys): + """Sort posts by a given predicate. Helper function for templates. + + If a key starts with '-', it is sorted in descending order. + + Usage examples:: + + sort_posts(timeline, 'title', 'date') + sort_posts(timeline, 'author', '-section_name') + """ + # We reverse the keys to get the usual ordering method: the first key + # provided is the most important sorting predicate (first by 'title', then + # by 'date' in the first example) + for key in reversed(keys): + if key.startswith('-'): + key = key[1:] + reverse = True + else: + reverse = False + try: + # An attribute (or method) of the Post object + a = getattr(posts[0], key) + if callable(a): + keyfunc = operator.methodcaller(key) + else: + keyfunc = operator.attrgetter(key) + except AttributeError: + # Post metadata + keyfunc = operator.methodcaller('meta', key) + + posts = sorted(posts, reverse=reverse, key=keyfunc) + return posts + + +def smartjoin(join_char: str, string_or_iterable) -> str: + """Join string_or_iterable with join_char if it is iterable; otherwise converts it to string. + + >>> smartjoin('; ', 'foo, bar') + 'foo, bar' + >>> smartjoin('; ', ['foo', 'bar']) + 'foo; bar' + >>> smartjoin(' to ', ['count', 42]) + 'count to 42' + """ + if isinstance(string_or_iterable, (str, bytes)): + return string_or_iterable + elif isinstance(string_or_iterable, Iterable): + return join_char.join([str(e) for e in string_or_iterable]) + else: + return str(string_or_iterable) + + +def _smartjoin_filter(string_or_iterable, join_char: str) -> str: + """Join stuff smartly, with reversed arguments for Jinja2 filters.""" + # http://jinja.pocoo.org/docs/2.10/api/#custom-filters + return smartjoin(join_char, string_or_iterable) + + # Stolen from textwrap in Python 3.4.3. def indent(text, prefix, predicate=None): """Add 'prefix' to the beginning of selected lines in 'text'. @@ -1924,11 +1922,13 @@ def load_data(path): """Given path to a file, load data from it.""" ext = os.path.splitext(path)[-1] loader = None + function = 'load' if ext in {'.yml', '.yaml'}: - loader = yaml - if yaml is None: - req_missing(['yaml'], 'use YAML data files') + if YAML is None: + req_missing(['ruamel.yaml'], 'use YAML data files') return {} + loader = YAML(typ='safe') + function = 'load' elif ext in {'.json', '.js'}: loader = json elif ext in {'.toml', '.tml'}: @@ -1938,5 +1938,141 @@ def load_data(path): loader = toml if loader is None: return - with io.open(path, 'r', encoding='utf8') as inf: - return loader.load(inf) + with io.open(path, 'r', encoding='utf-8-sig') as inf: + return getattr(loader, function)(inf) + + +def rss_writer(rss_obj, output_path): + """Write an RSS object to an xml file.""" + dst_dir = os.path.dirname(output_path) + makedirs(dst_dir) + with io.open(output_path, "w+", encoding="utf-8") as rss_file: + data = rss_obj.to_xml(encoding='utf-8') + if isinstance(data, bytes): + data = data.decode('utf-8') + rss_file.write(data) + + +def map_metadata(meta, key, config): + """Map metadata from other platforms to Nikola names. + + This uses the METADATA_MAPPING and METADATA_VALUE_MAPPING settings (via ``config``) and modifies the dict in place. + """ + for foreign, ours in config.get('METADATA_MAPPING', {}).get(key, {}).items(): + if foreign in meta: + meta[ours] = meta[foreign] + + for meta_key, hook in config.get('METADATA_VALUE_MAPPING', {}).get(key, {}).items(): + if meta_key in meta: + meta[meta_key] = hook(meta[meta_key]) + + +class ClassificationTranslationManager(object): + """Keeps track of which classifications could be translated as which others. + + The internal structure is as follows: + - per language, you have a map of classifications to maps + - the inner map is a map from other languages to sets of classifications + which are considered as translations + """ + + def __init__(self): + self._data = defaultdict(dict) + + def add_translation(self, translation_map): + """Add translation of one classification. + + ``translation_map`` must be a dictionary mapping languages to their + translations of the added classification. + """ + for lang, classification in translation_map.items(): + clmap = self._data[lang] + cldata = clmap.get(classification) + if cldata is None: + cldata = defaultdict(set) + clmap[classification] = cldata + for other_lang, other_classification in translation_map.items(): + if other_lang != lang: + cldata[other_lang].add(other_classification) + + def get_translations(self, classification, lang): + """Get a dict mapping other languages to (unsorted) lists of translated classifications.""" + clmap = self._data[lang] + cldata = clmap.get(classification) + if cldata is None: + return {} + else: + return {other_lang: list(classifications) for other_lang, classifications in cldata.items()} + + def get_translations_as_list(self, classification, lang, classifications_per_language): + """Get a list of pairs ``(other_lang, other_classification)`` which are translations of ``classification``. + + Avoid classifications not in ``classifications_per_language``. + """ + clmap = self._data[lang] + cldata = clmap.get(classification) + if cldata is None: + return [] + else: + result = [] + for other_lang, classifications in cldata.items(): + for other_classification in classifications: + if other_classification in classifications_per_language[other_lang]: + result.append((other_lang, other_classification)) + return result + + def has_translations(self, classification, lang): + """Return whether we know about the classification in that language. + + Note that this function returning ``True`` does not mean that + ``get_translations`` returns a non-empty dict or that + ``get_translations_as_list`` returns a non-empty list, but only + that this classification was explicitly added with + ``add_translation`` at some point. + """ + return self._data[lang].get(classification) is not None + + def add_defaults(self, posts_per_classification_per_language): + """Treat every classification as its own literal translation into every other language. + + ``posts_per_classification_per_language`` should be the first argument + to ``Taxonomy.postprocess_posts_per_classification``. + """ + # First collect all classifications from all languages + all_classifications = set() + for _, classifications in posts_per_classification_per_language.items(): + all_classifications.update(classifications.keys()) + # Next, add translation records for all of them + for classification in all_classifications: + record = {tlang: classification for tlang in posts_per_classification_per_language} + self.add_translation(record) + + def read_from_config(self, site, basename, posts_per_classification_per_language, add_defaults_default): + """Read translations from config. + + ``site`` should be the Nikola site object. Will consider + the variables ``<basename>_TRANSLATIONS`` and + ``<basename>_TRANSLATIONS_ADD_DEFAULTS``. + + ``posts_per_classification_per_language`` should be the first argument + to ``Taxonomy.postprocess_posts_per_classification``, i.e. this function + should be called from that function. ``add_defaults_default`` specifies + what the default value for ``<basename>_TRANSLATIONS_ADD_DEFAULTS`` is. + + Also sends signal via blinker to allow interested plugins to add + translations by themselves. The signal name used is + ``<lower(basename)>_translations_config``, and the argument is a dict + with entries ``translation_manager``, ``site`` and + ``posts_per_classification_per_language``. + """ + # Add translations + for record in site.config.get('{}_TRANSLATIONS'.format(basename), []): + self.add_translation(record) + # Add default translations + if site.config.get('{}_TRANSLATIONS_ADD_DEFAULTS'.format(basename), add_defaults_default): + self.add_defaults(posts_per_classification_per_language) + # Use blinker to inform interested parties (plugins) that they can add + # translations themselves + args = {'translation_manager': self, 'site': site, + 'posts_per_classification_per_language': posts_per_classification_per_language} + signal('{}_translations_config'.format(basename.lower())).send(args) diff --git a/nikola/winutils.py b/nikola/winutils.py index 6e341b8..a6506e6 100644 --- a/nikola/winutils.py +++ b/nikola/winutils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,7 +26,6 @@ """windows utilities to workaround problems with symlinks in a git clone.""" -from __future__ import print_function, unicode_literals import os import shutil import io @@ -67,11 +66,11 @@ def fix_all_git_symlinked(topdir): """ # Determine whether or not symlinks need fixing (they don’t if installing # from a .tar.gz file) - with io.open(topdir + r'\nikola\data\symlink-test-link.txt', 'r', encoding='utf-8') as f: + with io.open(topdir + r'\nikola\data\symlink-test-link.txt', 'r', encoding='utf-8-sig') as f: text = f.read() if text.startswith("NIKOLA_SYMLINKS=OK"): return -1 - with io.open(topdir + r'\nikola\data\symlinked.txt', 'r', encoding='utf-8') as f: + with io.open(topdir + r'\nikola\data\symlinked.txt', 'r', encoding='utf-8-sig') as f: text = f.read() # expect each line a relpath from git or zip root, # smoke test relpaths are relative to git root @@ -93,7 +92,7 @@ def fix_all_git_symlinked(topdir): continue # build src path and do some basic validation - with io.open(os.path.join(topdir, dst), 'r', encoding='utf-8') as f: + with io.open(os.path.join(topdir, dst), 'r', encoding='utf-8-sig') as f: text = f.read() dst_dir = os.path.dirname(dst) try: |
