aboutsummaryrefslogtreecommitdiffstats
path: root/nikola
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2015-07-08 07:35:06 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2015-07-08 07:35:06 -0300
commit055d72d76b44b0e627c8a17c48dbecd62e44197b (patch)
treee2c8d5475477c46115461fe9547c1ee797873635 /nikola
parent61f3aad02cd6492cb38e41b66f2ed8ec56e98981 (diff)
parentb0b24795b24ee6809397fbbadf42f31f310a219f (diff)
Merge tag 'upstream/7.6.0'
Upstream version 7.6.0
Diffstat (limited to 'nikola')
-rw-r--r--nikola/__init__.py4
-rw-r--r--nikola/__main__.py108
-rw-r--r--nikola/conf.py.in359
-rw-r--r--nikola/data/samplesite/README.txt6
-rw-r--r--nikola/data/samplesite/posts/1.rst21
-rw-r--r--nikola/data/samplesite/stories/1.rst4
-rw-r--r--nikola/data/samplesite/stories/bootstrap-demo.rst2
l---------nikola/data/samplesite/stories/upgrading-to-v6.txt1
l---------nikola/data/symlink-test-link.txt1
-rw-r--r--nikola/data/symlink-test-orig.txt4
-rw-r--r--nikola/data/symlinked.txt36
-rw-r--r--nikola/data/themes/base-jinja/templates/archiveindex.tmpl13
-rw-r--r--nikola/data/themes/base-jinja/templates/base.tmpl2
-rw-r--r--nikola/data/themes/base-jinja/templates/base_header.tmpl6
-rw-r--r--nikola/data/themes/base-jinja/templates/base_helper.tmpl31
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl12
-rw-r--r--nikola/data/themes/base-jinja/templates/crumbs.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/gallery.tmpl7
-rw-r--r--nikola/data/themes/base-jinja/templates/index.tmpl12
-rw-r--r--nikola/data/themes/base-jinja/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/base-jinja/templates/post.tmpl11
-rw-r--r--nikola/data/themes/base-jinja/templates/post_header.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/post_helper.tmpl37
-rw-r--r--nikola/data/themes/base-jinja/templates/post_list_directive.tmpl2
-rw-r--r--nikola/data/themes/base-jinja/templates/tag.tmpl15
-rw-r--r--nikola/data/themes/base-jinja/templates/tagindex.tmpl22
-rw-r--r--nikola/data/themes/base-jinja/templates/tags.tmpl26
-rw-r--r--nikola/data/themes/base/assets/css/nikola_ipython.css116
-rw-r--r--nikola/data/themes/base/assets/css/rst.css2
-rw-r--r--nikola/data/themes/base/assets/css/theme.css115
-rw-r--r--nikola/data/themes/base/assets/js/fancydates.js20
-rw-r--r--nikola/data/themes/base/assets/js/mathjax.js7
-rw-r--r--nikola/data/themes/base/assets/xml/atom.xsl28
-rw-r--r--nikola/data/themes/base/assets/xml/rss.xsl28
-rw-r--r--nikola/data/themes/base/messages/messages_ar.py49
-rw-r--r--nikola/data/themes/base/messages/messages_az.py39
-rw-r--r--nikola/data/themes/base/messages/messages_bg.py5
-rw-r--r--nikola/data/themes/base/messages/messages_ca.py27
-rw-r--r--nikola/data/themes/base/messages/messages_cs.py7
-rw-r--r--nikola/data/themes/base/messages/messages_da.py63
-rw-r--r--nikola/data/themes/base/messages/messages_de.py7
-rw-r--r--nikola/data/themes/base/messages/messages_el.py15
-rw-r--r--nikola/data/themes/base/messages/messages_en.py5
-rw-r--r--nikola/data/themes/base/messages/messages_eo.py5
-rw-r--r--nikola/data/themes/base/messages/messages_es.py37
-rw-r--r--nikola/data/themes/base/messages/messages_et.py5
-rw-r--r--nikola/data/themes/base/messages/messages_eu.py5
-rw-r--r--nikola/data/themes/base/messages/messages_fa.py9
-rw-r--r--nikola/data/themes/base/messages/messages_fi.py9
-rw-r--r--nikola/data/themes/base/messages/messages_fil.py39
-rw-r--r--nikola/data/themes/base/messages/messages_fr.py11
-rw-r--r--nikola/data/themes/base/messages/messages_gl.py5
-rw-r--r--nikola/data/themes/base/messages/messages_hi.py21
-rw-r--r--nikola/data/themes/base/messages/messages_hr.py5
-rw-r--r--nikola/data/themes/base/messages/messages_id.py39
-rw-r--r--nikola/data/themes/base/messages/messages_it.py25
-rw-r--r--nikola/data/themes/base/messages/messages_ja.py9
-rw-r--r--nikola/data/themes/base/messages/messages_ko.py39
-rw-r--r--nikola/data/themes/base/messages/messages_nb.py5
-rw-r--r--nikola/data/themes/base/messages/messages_nl.py5
-rw-r--r--nikola/data/themes/base/messages/messages_pl.py5
-rw-r--r--nikola/data/themes/base/messages/messages_pt.py5
-rw-r--r--nikola/data/themes/base/messages/messages_pt_br.py7
-rw-r--r--nikola/data/themes/base/messages/messages_ru.py7
-rw-r--r--nikola/data/themes/base/messages/messages_si_lk.py5
-rw-r--r--nikola/data/themes/base/messages/messages_sk.py7
-rw-r--r--nikola/data/themes/base/messages/messages_sl.py5
-rw-r--r--nikola/data/themes/base/messages/messages_sr.py39
-rw-r--r--nikola/data/themes/base/messages/messages_sv.py39
-rw-r--r--nikola/data/themes/base/messages/messages_tl.py39
-rw-r--r--nikola/data/themes/base/messages/messages_tr.py9
-rw-r--r--nikola/data/themes/base/messages/messages_uk.py39
-rw-r--r--nikola/data/themes/base/messages/messages_ur.py11
-rw-r--r--nikola/data/themes/base/messages/messages_zh_cn.py5
-rw-r--r--nikola/data/themes/base/messages/messages_zh_tw.py39
-rw-r--r--nikola/data/themes/base/templates/archiveindex.tmpl13
-rw-r--r--nikola/data/themes/base/templates/base.tmpl2
-rw-r--r--nikola/data/themes/base/templates/base_header.tmpl6
-rw-r--r--nikola/data/themes/base/templates/base_helper.tmpl33
-rw-r--r--nikola/data/themes/base/templates/comments_helper_disqus.tmpl14
-rw-r--r--nikola/data/themes/base/templates/crumbs.tmpl8
-rw-r--r--nikola/data/themes/base/templates/gallery.tmpl7
-rw-r--r--nikola/data/themes/base/templates/index.tmpl12
-rw-r--r--nikola/data/themes/base/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/base/templates/post.tmpl11
-rw-r--r--nikola/data/themes/base/templates/post_header.tmpl8
-rw-r--r--nikola/data/themes/base/templates/post_helper.tmpl37
-rw-r--r--nikola/data/themes/base/templates/post_list_directive.tmpl2
-rw-r--r--nikola/data/themes/base/templates/tag.tmpl15
-rw-r--r--nikola/data/themes/base/templates/tagindex.tmpl22
-rw-r--r--nikola/data/themes/base/templates/tags.tmpl26
-rw-r--r--nikola/data/themes/bootstrap-jinja/README.md2
-rw-r--r--[l---------]nikola/data/themes/bootstrap-jinja/assets/css/theme.css206
l---------nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js1
l---------nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js1
l---------nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js1
-rw-r--r--[l---------]nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js266
l---------nikola/data/themes/bootstrap-jinja/assets/js/jquery.js1
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/base.tmpl14
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl34
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl78
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl3
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/post.tmpl9
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl40
-rw-r--r--nikola/data/themes/bootstrap-jinja/templates/tags.tmpl26
-rw-r--r--nikola/data/themes/bootstrap/README.md2
-rw-r--r--nikola/data/themes/bootstrap/assets/css/theme.css48
l---------nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js1
l---------nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js1
l---------nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js1
l---------nikola/data/themes/bootstrap/assets/js/jquery.js1
-rw-r--r--nikola/data/themes/bootstrap/bundles4
-rw-r--r--nikola/data/themes/bootstrap/templates/base.tmpl14
-rw-r--r--nikola/data/themes/bootstrap/templates/base_helper.tmpl34
-rw-r--r--nikola/data/themes/bootstrap/templates/gallery.tmpl3
-rw-r--r--nikola/data/themes/bootstrap/templates/post.tmpl9
-rw-r--r--nikola/data/themes/bootstrap/templates/tags.tmpl26
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/docs.css161
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.pngbin70 -> 111 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.pngbin68 -> 215 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.pngbin69 -> 217 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.pngbin68 -> 108 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.pngbin69 -> 108 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.pngbin67 -> 111 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.pngbin65 -> 216 bytes
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.pngbin66 -> 214 bytes
l---------nikola/data/themes/bootstrap3-jinja/assets/css/rst.css1
-rw-r--r--[l---------]nikola/data/themes/bootstrap3-jinja/assets/css/theme.css214
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff21
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/base.tmpl16
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl36
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl6
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/rst.css318
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/theme.css60
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff21
-rw-r--r--nikola/data/themes/bootstrap3/bundles4
-rw-r--r--nikola/data/themes/bootstrap3/templates/base.tmpl16
-rw-r--r--nikola/data/themes/bootstrap3/templates/base_helper.tmpl36
-rw-r--r--nikola/data/themes/bootstrap3/templates/gallery.tmpl6
-rw-r--r--nikola/filters.py84
-rw-r--r--nikola/image_processing.py111
-rw-r--r--nikola/nikola.py971
-rw-r--r--nikola/packages/__init__.py1
-rw-r--r--nikola/packages/tzlocal/darwin.py13
-rw-r--r--nikola/packages/tzlocal/unix.py2
-rw-r--r--nikola/plugin_categories.py79
-rw-r--r--nikola/plugins/basic_import.py29
-rw-r--r--nikola/plugins/command/__init__.py2
-rw-r--r--nikola/plugins/command/auto.plugin2
-rw-r--r--nikola/plugins/command/auto.py87
-rw-r--r--nikola/plugins/command/auto/__init__.py366
l---------nikola/plugins/command/auto/livereload.js1
-rw-r--r--nikola/plugins/command/bootswatch_theme.plugin2
-rw-r--r--nikola/plugins/command/bootswatch_theme.py20
-rw-r--r--nikola/plugins/command/check.plugin2
-rw-r--r--nikola/plugins/command/check.py134
-rw-r--r--nikola/plugins/command/console.plugin2
-rw-r--r--nikola/plugins/command/console.py6
-rw-r--r--nikola/plugins/command/deploy.plugin2
-rw-r--r--nikola/plugins/command/deploy.py39
-rw-r--r--nikola/plugins/command/github_deploy.plugin2
-rw-r--r--nikola/plugins/command/github_deploy.py220
-rw-r--r--nikola/plugins/command/import_wordpress.plugin2
-rw-r--r--nikola/plugins/command/import_wordpress.py157
-rw-r--r--nikola/plugins/command/init.plugin2
-rw-r--r--nikola/plugins/command/init.py87
-rw-r--r--nikola/plugins/command/install_theme.plugin2
-rw-r--r--nikola/plugins/command/install_theme.py76
-rw-r--r--nikola/plugins/command/new_page.plugin2
-rw-r--r--nikola/plugins/command/new_page.py30
-rw-r--r--nikola/plugins/command/new_post.plugin2
-rw-r--r--nikola/plugins/command/new_post.py188
-rw-r--r--nikola/plugins/command/orphans.plugin2
-rw-r--r--nikola/plugins/command/orphans.py2
-rw-r--r--nikola/plugins/command/plugin.plugin2
-rw-r--r--nikola/plugins/command/plugin.py43
-rw-r--r--nikola/plugins/command/rst2html.plugin9
-rw-r--r--nikola/plugins/command/rst2html/__init__.py69
-rw-r--r--nikola/plugins/command/rst2html/rst2html.tmpl13
-rw-r--r--nikola/plugins/command/serve.plugin2
-rw-r--r--nikola/plugins/command/serve.py44
-rw-r--r--nikola/plugins/command/status.plugin9
-rw-r--r--nikola/plugins/command/status.py140
-rw-r--r--nikola/plugins/command/version.plugin2
-rw-r--r--nikola/plugins/command/version.py27
-rw-r--r--nikola/plugins/compile/__init__.py2
-rw-r--r--nikola/plugins/compile/html.plugin2
-rw-r--r--nikola/plugins/compile/html.py8
-rw-r--r--nikola/plugins/compile/ipynb.plugin8
-rw-r--r--nikola/plugins/compile/ipynb.py150
-rw-r--r--nikola/plugins/compile/ipynb/README.txt44
-rw-r--r--nikola/plugins/compile/ipynb/__init__.py97
-rw-r--r--nikola/plugins/compile/markdown.plugin2
-rw-r--r--nikola/plugins/compile/markdown/__init__.py12
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.py48
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.py4
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.py2
-rw-r--r--nikola/plugins/compile/pandoc.plugin2
-rw-r--r--nikola/plugins/compile/pandoc.py11
-rw-r--r--nikola/plugins/compile/php.plugin2
-rw-r--r--nikola/plugins/compile/php.py3
-rw-r--r--nikola/plugins/compile/rest.plugin2
-rw-r--r--nikola/plugins/compile/rest/__init__.py114
-rw-r--r--nikola/plugins/compile/rest/chart.py2
-rw-r--r--nikola/plugins/compile/rest/doc.py2
-rw-r--r--nikola/plugins/compile/rest/gist.py20
-rw-r--r--nikola/plugins/compile/rest/listing.py112
-rw-r--r--nikola/plugins/compile/rest/media.py2
-rw-r--r--nikola/plugins/compile/rest/post_list.py20
-rw-r--r--nikola/plugins/compile/rest/slides.py2
-rw-r--r--nikola/plugins/compile/rest/thumbnail.plugin9
-rw-r--r--nikola/plugins/compile/rest/thumbnail.py69
-rw-r--r--nikola/plugins/compile/rest/vimeo.py12
-rw-r--r--nikola/plugins/compile/rest/youtube.py2
-rw-r--r--nikola/plugins/loghandler/__init__.py2
-rw-r--r--nikola/plugins/loghandler/smtp.plugin2
-rw-r--r--nikola/plugins/loghandler/smtp.py2
-rw-r--r--nikola/plugins/loghandler/stderr.plugin2
-rw-r--r--nikola/plugins/loghandler/stderr.py2
-rw-r--r--nikola/plugins/misc/scan_posts.plugin10
-rw-r--r--nikola/plugins/misc/scan_posts.py100
-rw-r--r--nikola/plugins/task/__init__.py2
-rw-r--r--nikola/plugins/task/archive.plugin2
-rw-r--r--nikola/plugins/task/archive.py248
-rw-r--r--nikola/plugins/task/bundles.plugin2
-rw-r--r--nikola/plugins/task/bundles.py23
-rw-r--r--nikola/plugins/task/copy_assets.plugin2
-rw-r--r--nikola/plugins/task/copy_assets.py6
-rw-r--r--nikola/plugins/task/copy_files.plugin2
-rw-r--r--nikola/plugins/task/copy_files.py4
-rw-r--r--nikola/plugins/task/galleries.plugin2
-rw-r--r--nikola/plugins/task/galleries.py312
-rw-r--r--nikola/plugins/task/gzip.plugin2
-rw-r--r--nikola/plugins/task/gzip.py2
-rw-r--r--nikola/plugins/task/indexes.plugin2
-rw-r--r--nikola/plugins/task/indexes.py123
-rw-r--r--nikola/plugins/task/listings.plugin2
-rw-r--r--nikola/plugins/task/listings.py266
-rw-r--r--nikola/plugins/task/pages.plugin2
-rw-r--r--nikola/plugins/task/pages.py6
-rw-r--r--nikola/plugins/task/posts.plugin2
-rw-r--r--nikola/plugins/task/posts.py63
-rw-r--r--nikola/plugins/task/redirect.plugin2
-rw-r--r--nikola/plugins/task/redirect.py23
-rw-r--r--nikola/plugins/task/robots.plugin2
-rw-r--r--nikola/plugins/task/robots.py11
-rw-r--r--nikola/plugins/task/rss.plugin2
-rw-r--r--nikola/plugins/task/rss.py18
-rw-r--r--nikola/plugins/task/scale_images.plugin9
-rw-r--r--nikola/plugins/task/scale_images.py96
-rw-r--r--nikola/plugins/task/sitemap.plugin2
-rw-r--r--nikola/plugins/task/sitemap/__init__.py106
-rw-r--r--nikola/plugins/task/sources.plugin2
-rw-r--r--nikola/plugins/task/sources.py7
-rw-r--r--nikola/plugins/task/tags.plugin2
-rw-r--r--nikola/plugins/task/tags.py301
-rw-r--r--nikola/plugins/template/__init__.py2
-rw-r--r--nikola/plugins/template/jinja.plugin2
-rw-r--r--nikola/plugins/template/jinja.py5
-rw-r--r--nikola/plugins/template/mako.plugin2
-rw-r--r--nikola/plugins/template/mako.py4
-rw-r--r--nikola/post.py287
-rw-r--r--nikola/utils.py477
-rw-r--r--nikola/winutils.py16
264 files changed, 7383 insertions, 2791 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py
index 4cb5427..263874f 100644
--- a/nikola/__init__.py
+++ b/nikola/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,7 +27,7 @@
from __future__ import absolute_import
import os
-__version__ = "7.1.0"
+__version__ = "7.6.0"
DEBUG = bool(os.getenv('NIKOLA_DEBUG'))
from .nikola import Nikola # NOQA
diff --git a/nikola/__main__.py b/nikola/__main__.py
index f492800..6aa0977 100644
--- a/nikola/__main__.py
+++ b/nikola/__main__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,7 +25,7 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function, unicode_literals
-from operator import attrgetter
+from collections import defaultdict
import os
import shutil
try:
@@ -49,10 +49,18 @@ from blinker import signal
from . import __version__
from .plugin_categories import Command
from .nikola import Nikola
-from .utils import _reload, sys_decode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, ColorfulStderrHandler
+from .utils import sys_decode, sys_encode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, ColorfulStderrHandler
+
+if sys.version_info[0] == 3:
+ import importlib.machinery
+else:
+ import imp
config = {}
+# DO NOT USE unless you know what you are doing!
+_RETURN_DOITNIKOLA = False
+
def main(args=None):
colorful = False
@@ -63,16 +71,34 @@ def main(args=None):
if args is None:
args = sys.argv[1:]
+
+ oargs = args
+ 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
- if len(args) > 0 and args[0] == b'build' and b'--strict' in args:
+ if len(args) > 0 and args[0] == 'build' and '--strict' in args:
LOGGER.notice('Running in strict mode')
STRICT_HANDLER.push_application()
- if len(args) > 0 and args[0] == b'build' and b'-q' in args or b'--quiet' in args:
+ if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
nullhandler = NullHandler()
nullhandler.push_application()
quiet = True
global config
+ original_cwd = os.getcwd()
+
# Those commands do not require a `conf.py`. (Issue #1132)
# Moreover, actually having one somewhere in the tree can be bad, putting
# the output of that command (the new site) in an unknown directory that is
@@ -82,25 +108,38 @@ def main(args=None):
root = get_root_dir()
if root:
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_')
+ else:
+ needs_config_file = False
sys.path.append('')
try:
- import conf
- _reload(conf)
+ 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)
config = conf.__dict__
except Exception:
- if os.path.exists('conf.py'):
+ if os.path.exists(conf_filename):
msg = traceback.format_exc(0)
- LOGGER.error('conf.py cannot be parsed.\n{0}'.format(msg))
- sys.exit(1)
+ LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg))
+ return 1
+ elif needs_config_file and conf_filename_changed:
+ LOGGER.error('Cannot find configuration file "{0}".'.format(conf_filename))
+ return 1
config = {}
+ if conf_filename_changed:
+ LOGGER.info("Using config file '{0}'".format(conf_filename))
+
invariant = False
- if len(args) > 0 and args[0] == b'build' and b'--invariant' in args:
+ if len(args) > 0 and args[0] == 'build' and '--invariant' in args:
try:
import freezegun
- freeze = freezegun.freeze_time("2014-01-01")
+ freeze = freezegun.freeze_time("2038-01-01")
freeze.start()
invariant = True
except ImportError:
@@ -114,9 +153,13 @@ def main(args=None):
config['__colorful__'] = colorful
config['__invariant__'] = invariant
config['__quiet__'] = quiet
-
+ config['__configuration_filename__'] = conf_filename
+ config['__cwd__'] = original_cwd
site = Nikola(**config)
- _ = DoitNikola(site, quiet).run(args)
+ DN = DoitNikola(site, quiet)
+ if _RETURN_DOITNIKOLA:
+ return DN
+ _ = DN.run(oargs)
if site.invariant:
freeze.stop()
@@ -135,10 +178,11 @@ class Help(DoitHelp):
# --strict, --invariant and --quiet.
del cmds['run']
- print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com/\n\n")
+ print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit https://getnikola.com/\n\n")
print("Available commands:")
- for cmd in sorted(cmds.values(), key=attrgetter('name')):
- print(" nikola %-*s %s" % (20, cmd.name, cmd.doc_purpose))
+ for cmd_name in sorted(cmds.keys()):
+ cmd = cmds[cmd_name]
+ print(" nikola {:20s} {}".format(cmd_name, cmd.doc_purpose))
print("")
print(" nikola help show help / reference")
print(" nikola help <command> show command usage")
@@ -214,12 +258,13 @@ class NikolaTaskLoader(TaskLoader):
'outfile': sys.stderr,
}
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 executes after site is rendered.'))
+ self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
signal('initialized').send(self.nikola)
return tasks + latetasks, DOIT_CONFIG
@@ -230,20 +275,21 @@ class DoitNikola(DoitMain):
TASK_LOADER = NikolaTaskLoader
def __init__(self, nikola, quiet=False):
+ super(DoitNikola, self).__init__()
self.nikola = nikola
nikola.doit = self
self.task_loader = self.TASK_LOADER(nikola, quiet)
- def get_commands(self):
+ def get_cmds(self):
# core doit commands
- cmds = DoitMain.get_commands(self)
+ cmds = DoitMain.get_cmds(self)
# load nikola commands
for name, cmd in self.nikola._commands.items():
cmds[name] = cmd
return cmds
def run(self, cmd_args):
- sub_cmds = self.get_commands()
+ sub_cmds = self.get_cmds()
args = self.process_args(cmd_args)
args = [sys_decode(arg) for arg in args]
@@ -270,18 +316,30 @@ class DoitNikola(DoitMain):
args = ['version']
if args[0] not in sub_cmds.keys():
LOGGER.error("Unknown command {0}".format(args[0]))
- return False
- if not isinstance(sub_cmds[args[0]], (Command, Help)): # Is a doit command
+ sugg = defaultdict(list)
+ for c in sub_cmds.keys():
+ d = lev(c, args[0])
+ sugg[d].append(c)
+ LOGGER.info('Did you mean "{}"?', '" or "'.join(sugg[min(sugg.keys())]))
+ return 3
+ if sub_cmds[args[0]] is not Help and not isinstance(sub_cmds[args[0]], Command): # Is a doit command
if not self.nikola.configured:
LOGGER.error("This command needs to run inside an "
"existing Nikola site.")
- return False
-
+ return 3
return super(DoitNikola, self).run(cmd_args)
@staticmethod
def print_version():
print("Nikola v" + __version__)
+
+# Stolen from http://stackoverflow.com/questions/4173579/implementing-levenshtein-distance-in-python
+def lev(a, b):
+ if not a or not b:
+ return max(len(a), len(b))
+ return min(lev(a[1:], b[1:]) + (a[0] != b[0]), lev(a[1:], b) + 1, lev(a, b[1:]) + 1)
+
+
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
diff --git a/nikola/conf.py.in b/nikola/conf.py.in
index 04c2098..ab00673 100644
--- a/nikola/conf.py.in
+++ b/nikola/conf.py.in
@@ -23,7 +23,7 @@ BLOG_TITLE = ${BLOG_TITLE} # (translatable)
# This is the main URL for your site. It will be used
# in a prominent link
SITE_URL = ${SITE_URL}
-# This is the URL where nikola's output will be deployed.
+# This is the URL where Nikola's output will be deployed.
# If not set, defaults to SITE_URL
# BASE_URL = ${SITE_URL}
BLOG_EMAIL = ${BLOG_EMAIL}
@@ -63,19 +63,26 @@ TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN}
# This is a dict. The keys are languages, and values are tuples.
#
# For regular links:
-# ('http://example.com/', 'Text')
+# ('https://getnikola.com/', 'Nikola Homepage')
#
# For submenus:
-# ((
-# ('Sub 1', 'http://example.com/'),
-# ('Sub 2', 'http://example.org/'),
-# ), 'Top')
+# (
+# (
+# ('http://apple.com/', 'Apple'),
+# ('http://orange.com/', 'Orange'),
+# ),
+# 'Fruits'
+# )
#
# WARNING: Support for submenus is theme-dependent.
# Only one level of submenus is supported.
# WARNING: Some themes, including the default Bootstrap 3 theme,
# may present issues if the menu is too large.
# (in bootstrap3, 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
+# else they won’t be highlighted when active.
NAVIGATION_LINKS = ${NAVIGATION_LINKS}
@@ -90,7 +97,7 @@ THEME = ${THEME}
# another time zone, please set TIMEZONE to match. Check the available
# list from Wikipedia:
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
-# (eg. 'Europe/Zurich')
+# (e.g. 'Europe/Zurich')
# Also, if you want to use a different time zone in some of your posts,
# you can use the ISO 8601/RFC 3339 format (ex. 2012-03-30T23:00:00+02:00)
TIMEZONE = ${TIMEZONE}
@@ -104,13 +111,26 @@ TIMEZONE = ${TIMEZONE}
# (str used by datetime.datetime.strftime)
# DATE_FORMAT = '%Y-%m-%d %H:%M'
-# While nikola can select a sensible locale for each language,
+# Date format used to display post dates, if local dates are used.
+# (str used by moment.js)
+# JS_DATE_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”
+#
+# Your theme must support it, bootstrap and bootstrap3 already do.
+# 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.
+# "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 ommit one or more keys.
+# 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.
@@ -121,7 +141,7 @@ TIMEZONE = ${TIMEZONE}
# (whatever/thing.txt).
#
# That fragment could have an associated metadata file (whatever/thing.meta),
-# and optionally translated files (example for spanish, with code "es"):
+# and optionally translated files (example for Spanish, with code "es"):
# whatever/thing.es.txt and whatever/thing.es.meta
#
# This assumes you use the default TRANSLATIONS_PATTERN.
@@ -129,7 +149,7 @@ TIMEZONE = ${TIMEZONE}
# From those files, a set of HTML fragment files will be generated:
# cache/whatever/thing.html (and maybe cache/whatever/thing.html.es)
#
-# These files are combinated with the template to produce rendered
+# These files are combined with the template to produce rendered
# pages, which will be placed at
# output / TRANSLATIONS[lang] / destination / pagename.html
#
@@ -144,18 +164,24 @@ POSTS = ${POSTS}
PAGES = ${PAGES}
# One or more folders containing files to be copied as-is into the output.
-# The format is a dictionary of "source" "relative destination".
+# The format is a dictionary of {source: relative destination}.
# Default is:
-# FILES_FOLDERS = {'files': '' }
+# FILES_FOLDERS = {'files': ''}
# Which means copy 'files' into 'output'
+# One or more folders containing listings to be processed and stored into
+# the output. The format is a dictionary of {source: relative destination}.
+# Default is:
+# LISTINGS_FOLDERS = {'listings': 'listings'}
+# Which means process listings from 'listings' into 'output/listings'
+
# A mapping of languages to file-extensions that represent that language.
# 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.
#
# 'rest' is reStructuredText
# 'markdown' is MarkDown
-# 'html' assumes the file is html and just copies it
+# 'html' assumes the file is HTML and just copies it
COMPILERS = ${COMPILERS}
# Create by default posts in one file format?
@@ -178,6 +204,10 @@ COMPILERS = ${COMPILERS}
# 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
+
# Paths for different autogenerated bits. These are combined with the
# translation paths.
@@ -191,6 +221,64 @@ COMPILERS = ${COMPILERS}
# the posts themselves. If set to False, it will be just a list of links.
# TAG_PAGES_ARE_INDEXES = False
+# 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 = {
+# DEFAULT_LANG: {
+# "blogging": "Meta-blog posts about blogging about blogging.",
+# "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects."
+# },
+#}
+
+
+# If you do not want to display a tag publicly, you can mark it as hidden.
+# The tag will not be displayed on the tag list page, the tag cloud and posts.
+# Tag pages will still be generated.
+HIDDEN_TAGS = ['mathjax']
+
+# Only include tags on the tag list/overview page if there are at least
+# TAGLIST_MINIMUM_POSTS number of posts or more with every tag. Every tag
+# page is still generated, linked from posts, and included in the sitemap.
+# However, more obscure tags can be hidden from the tag index page.
+# TAGLIST_MINIMUM_POSTS = 1
+
+# 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)
+# CATEGORY_PATH = "categories"
+# CATEGORY_PREFIX = "cat_"
+
+# 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
+# a forward slash or a backslash (i.e. '\//\\' is a path specifying the
+# subcategory called '\' of the top-level category called '/').
+# CATEGORY_ALLOW_HIERARCHIES = False
+# If CATEGORY_OUTPUT_FLAT_HIERARCHY is set to True, the output written to output
+# contains only the name of the leaf category and not the whole path.
+# CATEGORY_OUTPUT_FLAT_HIERARCHY = False
+
+# If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain
+# the posts themselves. If set to False, it will be just a list of links.
+# CATEGORY_PAGES_ARE_INDEXES = False
+
+# 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 = {
+# DEFAULT_LANG: {
+# "blogging": "Meta-blog posts about blogging about blogging.",
+# "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects."
+# },
+#}
+
+# If you do not want to display a category publicly, you can mark it as hidden.
+# The category will not be displayed on the category list page.
+# Category pages will still be generated.
+HIDDEN_CATEGORIES = []
+
# Final location for the main blog page and sibling paginated pages is
# output / TRANSLATION[lang] / INDEX_PATH / index-*.html
# INDEX_PATH = ""
@@ -199,13 +287,24 @@ COMPILERS = ${COMPILERS}
# CREATE_MONTHLY_ARCHIVE = False
# Create one large archive instead of per-year
# CREATE_SINGLE_ARCHIVE = False
+# Create year, month, and day archives each with a (long) list of posts
+# (overrides both CREATE_MONTHLY_ARCHIVE and CREATE_SINGLE_ARCHIVE)
+# CREATE_FULL_ARCHIVES = False
+# If monthly archives or full archives are created, adds also one archive per day
+# CREATE_DAILY_ARCHIVE = 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
# ARCHIVE_PATH = ""
# ARCHIVE_FILENAME = "archive.html"
+# If ARCHIVES_ARE_INDEXES is set to True, each archive page which contains a list
+# of posts will contain the posts themselves. If set to False, it will be just a
+# list of links.
+# ARCHIVES_ARE_INDEXES = False
+
# URLs to other posts/pages can take 3 forms:
# rel_path: a relative URL to the current page/post (default)
# full_path: a URL with the full path from the root
@@ -232,15 +331,23 @@ COMPILERS = ${COMPILERS}
# If you don't need any of these, just set to []
REDIRECTIONS = ${REDIRECTIONS}
-# Commands to execute to deploy. Can be anything, for example,
-# you may use rsync:
+# Presets of commands to execute to deploy. Can be anything, for
+# example, you may use rsync:
# "rsync -rav --delete output/ joe@my.site:/srv/www/site"
# And then do a backup, or run `nikola ping` from the `ping`
-# plugin (`nikola install_plugin ping`).
-# To do manual deployment, set it to []
-# DEPLOY_COMMANDS = []
+# plugin (`nikola plugin -i ping`). Or run `nikola check -l`.
+# You may also want to use github_deploy (see below).
+# You can define multiple presets and specify them as arguments
+# to `nikola deploy`. If no arguments are specified, a preset
+# named `default` will be executed. You can use as many presets
+# in a `nikola deploy` command as you like.
+# DEPLOY_COMMANDS = {
+# 'default': [
+# "rsync -rav --delete output/ joe@my.site:/srv/www/site",
+# ]
+# }
-# For user.github.io/organization.github.io pages, the DEPLOY branch
+# For user.github.io OR organization.github.io pages, the DEPLOY branch
# MUST be 'master', and 'gh-pages' for other repositories.
# GITHUB_SOURCE_BRANCH = 'master'
# GITHUB_DEPLOY_BRANCH = 'gh-pages'
@@ -278,7 +385,7 @@ REDIRECTIONS = ${REDIRECTIONS}
# Nikola’s templates. All other filters must be enabled through FILTERS.
#
# Many filters are shipped with Nikola. A list is available in the manual:
-# <http://getnikola.com/handbook.html#post-processing-filters>
+# <https://getnikola.com/handbook.html#post-processing-filters>
#
# from nikola import filters
# FILTERS = {
@@ -291,7 +398,7 @@ REDIRECTIONS = ${REDIRECTIONS}
# side optimization for very high traffic sites or low memory servers.
# GZIP_FILES = False
# File extensions that will be compressed
-# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml')
+# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.atom', '.xml')
# Use an external gzip command? None means no.
# Example: GZIP_COMMAND = "pigz -k {filename}"
# GZIP_COMMAND = None
@@ -318,9 +425,13 @@ REDIRECTIONS = ${REDIRECTIONS}
# Image Gallery Options
# #############################################################################
-# Galleries are folders in galleries/
-# Final location of galleries will be output / GALLERY_PATH / gallery_name
-# GALLERY_PATH = "galleries"
+# One or more folders containing galleries. The format is a dictionary of
+# {"source": "relative_destination"}, where galleries are looked for in
+# "source/" and the results will be located in
+# "OUTPUT_PATH/relative_destination/gallery_name"
+# Default is:
+# GALLERY_FOLDERS = {"galleries": "galleries"}
+# More gallery options:
# THUMBNAIL_SIZE = 180
# MAX_IMAGE_SIZE = 1280
# USE_FILENAME_AS_TITLE = True
@@ -328,23 +439,94 @@ REDIRECTIONS = ${REDIRECTIONS}
#
# If set to False, it will sort by filename instead. Defaults to True
# GALLERY_SORT_BY_DATE = True
+#
+# Folders containing images to be used in normal posts or pages. 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 format is a dictionary of {source: relative destination}.
+
+IMAGE_FOLDERS = {'images': 'images'}
+# IMAGE_THUMBNAIL_SIZE = 400
# #############################################################################
# HTML fragments and diverse things that are used by the templates
# #############################################################################
# Data about post-per-page indexes.
-# INDEXES_PAGES defaults to 'old posts, page %d' or 'page %d' (translated),
+# INDEXES_PAGES defaults to ' old posts, page %d' or ' page %d' (translated),
# depending on the value of INDEXES_PAGES_MAIN.
-# INDEXES_TITLE = "" # If this is empty, defaults to BLOG_TITLE
-# INDEXES_PAGES = "" # If this is empty, defaults to '[old posts,] page %d' (see above)
-# INDEXES_PAGES_MAIN = False # If True, INDEXES_PAGES is also displayed on
-# # the main (the newest) index page (index.html)
+#
+# (translatable) If the following is empty, defaults to BLOG_TITLE:
+# INDEXES_TITLE = ""
+#
+# (translatable) If the following is empty, defaults to ' [old posts,] page %d' (see above):
+# INDEXES_PAGES = ""
+#
+# If the following is True, INDEXES_PAGES is also displayed on the main (the
+# newest) index page (index.html):
+# INDEXES_PAGES_MAIN = False
+#
+# If the following is True, index-1.html has the oldest posts, index-2.html the
+# second-oldest posts, etc., and index.html has the newest posts. This ensures
+# that all posts on index-x.html will forever stay on that page, now matter how
+# many new posts are added.
+# If False, index-1.html has the second-newest posts, index-2.html the third-newest,
+# and index-n.html the oldest posts. When this is active, old posts can be moved
+# to other index pages when new posts are added.
+# INDEXES_STATIC = True
+#
+# (translatable) If PRETTY_URLS is set to True, this setting will be used to create
+# prettier URLs for index pages, such as page/2/index.html instead of index-2.html.
+# Valid values for this settings are:
+# * False,
+# * a list or tuple, specifying the path to be generated,
+# * a dictionary mapping languages to lists or tuples.
+# Every list or tuple must consist of strings which are used to combine the path;
+# for example:
+# ['page', '{number}', '{index_file}']
+# The replacements
+# {number} --> (logical) page number;
+# {old_number} --> the page number inserted into index-n.html before (zero for
+# the main page);
+# {index_file} --> value of option INDEX_FILE
+# are made.
+# Note that in case INDEXES_PAGES_MAIN is set to True, a redirection will be created
+# for the full URL with the page number of the main page to the normal (shorter) main
+# page URL.
+# INDEXES_PRETTY_PAGE_URL = False
# Color scheme to be used for code blocks. If your theme provides
# "assets/css/code.css" this is ignored.
-# Can be any of autumn borland bw colorful default emacs friendly fruity manni
-# monokai murphy native pastie perldoc rrt tango trac vim vs
+# 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
+# This list MAY be incomplete since pygments adds styles every now and then.
# CODE_COLOR_SCHEME = 'default'
# If you use 'site-reveal' theme you can select several subthemes
@@ -357,12 +539,12 @@ REDIRECTIONS = ${REDIRECTIONS}
# You can also use: page/concave/linear/none/default
# FAVICONS contains (name, file, size) tuples.
-# Used for create favicon link like this:
+# Used to create favicon link like this:
# <link rel="name" href="file" sizes="size"/>
-# FAVICONS = {
+# FAVICONS = (
# ("icon", "/favicon.ico", "16x16"),
# ("icon", "/icon_128x128.png", "128x128"),
-# }
+# )
# Show only teasers in the index pages? Defaults to False.
# INDEX_TEASERS = False
@@ -384,6 +566,16 @@ INDEX_READ_MORE_LINK = ${INDEX_READ_MORE_LINK}
# 'Read more...' for the RSS_FEED, if RSS_TEASERS is True (translatable)
RSS_READ_MORE_LINK = ${RSS_READ_MORE_LINK}
+# Append a URL query to the RSS_READ_MORE_LINK in Atom and RSS feeds. Advanced
+# option used for traffic source tracking.
+# Minimum example for use with Piwik: "pk_campaign=feed"
+# The following tags exist and are replaced for you:
+# {feedRelUri} A relative link to the feed.
+# {feedFormat} The name of the syndication format.
+# Example using replacement for use with Google Analytics:
+# "utm_source={feedRelUri}&utm_medium=nikola_feed&utm_campaign={feedFormat}_feed"
+RSS_LINKS_APPEND_QUERY = False
+
# A HTML fragment describing the license, for the sidebar.
# (translatable)
LICENSE = ""
@@ -399,7 +591,7 @@ LICENSE = ""
# (translatable)
CONTENT_FOOTER = 'Contents &copy; {date} \
<a href="mailto:{email}">{author}</a> - Powered by \
- <a href="http://getnikola.com" rel="nofollow">Nikola</a> \
+ <a href="https://getnikola.com" rel="nofollow">Nikola</a> \
{license}'
# Things that will be passed to CONTENT_FOOTER.format(). This is done
@@ -443,8 +635,8 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# the "noannotations" metadata.
# ANNOTATIONS = False
-# Create index.html for story folders?
-# WARNING: if a story would conflict with the index file (usually
+# Create index.html for page (story) folders?
+# WARNING: if a page would conflict with the index file (usually
# caused by setting slug to `index`), the STORY_INDEX
# will not be generated for that directory.
# STORY_INDEX = False
@@ -480,12 +672,12 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# /robots.txt and /sitemap.xml, and to inform search engines about /sitemapindex.xml.
# ROBOTS_EXCLUSIONS = ["/archive.html", "/category/*.html"]
-# Instead of putting files in <slug>.html, put them in
-# <slug>/index.html. Also enables STRIP_INDEXES
+# Instead of putting files in <slug>.html, put them in <slug>/index.html.
+# No web server configuration is required. Also enables STRIP_INDEXES.
# This can be disabled on a per-page/post basis by adding
# .. pretty_url: False
-# to the metadata
-# PRETTY_URLS = False
+# to the metadata.
+PRETTY_URLS = ${PRETTY_URLS}
# If True, publish future dated posts right away instead of scheduling them.
# Defaults to False.
@@ -513,7 +705,8 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# MathJax.Hub.Config({
# tex2jax: {
# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ],
-# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ]
+# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ],
+# processEscapes: true
# },
# displayAlign: 'left', // Change this to 'center' to center equations.
# "HTML-CSS": {
@@ -525,7 +718,7 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# Do you want to customize the nbconversion of your IPython notebook?
# IPYNB_CONFIG = {}
-# With the following example configuracion you can use a custom jinja template
+# 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'}}
@@ -534,10 +727,17 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# done in the code, hope you don't mind ;-)
# Note: most Nikola-specific extensions are done via the Nikola plugin system,
# with the MarkdownExtension class and should not be added here.
-# MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite']
+# The default is ['fenced_code', 'codehilite']
+MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra']
+
+# Extra options to pass to the pandoc comand.
+# by default, it's empty, is a list of strings, for example
+# ['-F', 'pandoc-citeproc', '--bibliography=/Users/foo/references.bib']
+# PANDOC_OPTIONS = []
# Social buttons. This is sample code for AddThis (which was the default for a
-# long time). Insert anything you want here, or even make it empty.
+# long time). Insert anything you want here, or even make it empty (which is
+# the default right now)
# (translatable)
# SOCIAL_BUTTONS_CODE = """
# <!-- Social buttons -->
@@ -568,20 +768,29 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# links to it. Set this to False to disable everything RSS-related.
# GENERATE_RSS = True
+# By default, Nikola does not generates Atom files for indexes and links to
+# them. Generate Atom for tags by setting TAG_PAGES_ARE_INDEXES to True.
+# Atom feeds are built based on INDEX_DISPLAY_POST_COUNT and not FEED_LENGTH
+# Switch between plain-text summaries and full HTML content using the
+# RSS_TEASER option. RSS_LINKS_APPEND_QUERY is also respected. Atom feeds
+# are generated even for old indexes and have pagination link relations
+# between each other. Old Atom feeds with no changes are marked as archived.
+# GENERATE_ATOM = False
+
# 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.
+# change it for a FeedBurner feed or something else.
# RSS_LINK = None
-# Show only teasers in the RSS feed? Default to True
+# Show only teasers in the RSS and Atom feeds? Default to True
# RSS_TEASERS = True
# Strip HTML in the RSS feed? Default to False
# RSS_PLAIN = False
-# A search form to search this site, for the sidebar. You can use a google
+# A search form to search this site, for the sidebar. You can use a Google
# custom search (http://www.google.com/cse/)
-# Or a duckduckgo search: https://duckduckgo.com/search_box.html
+# Or a DuckDuckGo search: https://duckduckgo.com/search_box.html
# Default is no search form.
# (translatable)
# SEARCH_FORM = ""
@@ -604,9 +813,9 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# <!-- End of custom search -->
# """ % SITE_URL
#
-# If you prefer a google search form, here's an example that should just work:
+# If you prefer a Google search form, here's an example that should just work:
# SEARCH_FORM = """
-# <!-- Custom search with google-->
+# <!-- Custom search with Google-->
# <form id="search" action="//www.google.com/search" method="get" class="navbar-form pull-left">
# <input type="hidden" name="q" value="site:%s" />
# <input type="text" name="q" maxlength="255" results="0" placeholder="Search"/>
@@ -614,14 +823,20 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# <!-- End of custom search -->
#""" % SITE_URL
-# Use content distribution networks for jquery, twitter-bootstrap css and js,
+# Use content distribution networks for jQuery, twitter-bootstrap css and js,
# and html5shiv (for older versions of Internet Explorer)
-# If this is True, jquery and html5shiv is served from the Google and twitter-
-# bootstrap is served from the NetDNA CDN
+# If this is True, jQuery and html5shiv are served from the Google CDN and
+# Bootstrap is served from BootstrapCDN (provided by MaxCDN)
# Set this to False if you want to host your site without requiring access to
# external resources.
# USE_CDN = False
+# Check for USE_CDN compatibility.
+# If you are using custom themes, have configured the CSS properly and are
+# receiving warnings about incompatibility but believe they are incorrect, you
+# can set this to False.
+# USE_CDN_WARNING = True
+
# Extra things you want in the pages HEAD tag. This will be added right
# before </head>
# (translatable)
@@ -658,29 +873,28 @@ UNSLUGIFY_TITLES = True
# Open Graph is enabled by default.
# USE_OPEN_GRAPH = True
-# Nikola supports Twitter Card summaries
-# Twitter cards are disabled by default. They make it possible for you to
-# attach media to Tweets that link to your content.
+# 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://dev.twitter.com/form/participate-twitter-cards
+# To do this please visit https://cards-dev.twitter.com/validator
#
# Uncomment and modify to following lines to match your accounts.
-# Specifying the id for either 'site' or 'creator' will be preferred
-# over the cleartext username. Specifying an ID is not necessary.
-# Displaying images is currently not supported.
+# Images displayed come from the `previewimage` meta tag.
+# You can specify the card type by using the `card` parameter in TWITTER_CARD.
# TWITTER_CARD = {
# # 'use_twitter_cards': True, # enable Twitter Cards
-# # 'site': '@website', # twitter nick for the website
-# # 'site:id': 123456, # Same as site, but the website's Twitter user ID
-# # instead.
-# # 'creator': '@username', # Username for the content creator / author.
-# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
+# # 'card': 'summary', # Card type, you can also use 'summary_large_image',
+# # see https://dev.twitter.com/cards/types
+# # 'site': '@website', # twitter nick for the website
+# # 'creator': '@username', # Username for the content creator / author.
# }
-# If webassets is installed, bundle JS and CSS to make site loading faster
+# 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.
# USE_BUNDLES = True
# Plugins you don't want to use. Be careful :-)
@@ -740,3 +954,8 @@ LOGGING_HANDLERS = {
# Put in global_context things you want available on all your templates.
# It can be anything, data, functions, modules, etc.
GLOBAL_CONTEXT = {}
+
+# Add functions here and they will be called with template
+# GLOBAL_CONTEXT as parameter when the template is about to be
+# rendered
+GLOBAL_CONTEXT_FILLER = []
diff --git a/nikola/data/samplesite/README.txt b/nikola/data/samplesite/README.txt
index aeea39c..da0d685 100644
--- a/nikola/data/samplesite/README.txt
+++ b/nikola/data/samplesite/README.txt
@@ -1,6 +1,6 @@
-This folder contains the source used to generate a static site by nikola.
+This folder contains the source used to generate a static site using Nikola.
-Installation and documentation at http://getnikola.com
+Installation and documentation at https://getnikola.com/
Configuration file for the site is `conf.py`.
@@ -12,7 +12,7 @@ To see it::
nikola serve
-And point your browser to http://localhost:8000
+And point your browser to http://localhost:8000/
To check all available commands::
diff --git a/nikola/data/samplesite/posts/1.rst b/nikola/data/samplesite/posts/1.rst
index 7116a7a..9a55859 100644
--- a/nikola/data/samplesite/posts/1.rst
+++ b/nikola/data/samplesite/posts/1.rst
@@ -3,22 +3,25 @@
.. date: 2012-03-30 23:00:00 UTC-03:00
.. tags: nikola, python, demo, blog
.. author: Roberto Alsina
-.. link: http://getnikola.com
+.. link: https://getnikola.com/
.. description:
+.. category: nikola
.. figure:: http://farm1.staticflickr.com/138/352972944_4f9d568680.jpg
:target: http://farm1.staticflickr.com/138/352972944_4f9d568680_z.jpg?zz=1
:class: thumbnail
:alt: Nikola Tesla Corner by nicwest, on Flickr
-If you can see this in a web browser, it means you have managed to install Nikola,
+If you can see this in a web browser, it means you managed to install Nikola,
and build a site using it. Congratulations!
-* You can read the manual `here </stories/handbook.html>`__
-* You can learn more about Nikola at http://getnikola.com
-* You can see a demo photo gallery `here </galleries/demo/index.html>`__
-* Demo usage of listings `here </stories/listings-demo.html>`__
-* Demo of slideshows `here </stories/slides-demo.html>`__
-* Demo of Bootstrap `here </stories/bootstrap-demo.html>`__
+Next steps:
-Send feedback to ralsina@netmanagers.com.ar!
+* `Read the manual </stories/handbook.html>`__
+* `Visit the Nikola website to learn more <https://getnikola.com>`__
+* `See a demo photo gallery </galleries/demo/index.html>`__
+* `See a demo listing </stories/listings-demo.html>`__
+* `See a demo slideshow </stories/slides-demo.html>`__
+* `See a demo of the Bootstrap theme </stories/bootstrap-demo.html>`__
+
+Send feedback to info@getnikola.com!
diff --git a/nikola/data/samplesite/stories/1.rst b/nikola/data/samplesite/stories/1.rst
index b662fae..abe1d0b 100644
--- a/nikola/data/samplesite/stories/1.rst
+++ b/nikola/data/samplesite/stories/1.rst
@@ -7,5 +7,5 @@
Hope you enjoy this software!
-* Home page at http://getnikola.com
-* Author's blog (and reason why Nikola exists): http://ralsina.me
+* Home page at https://getnikola.com/
+* Author's blog (and reason why Nikola exists): http://ralsina.me/
diff --git a/nikola/data/samplesite/stories/bootstrap-demo.rst b/nikola/data/samplesite/stories/bootstrap-demo.rst
index a7be1a9..481140a 100644
--- a/nikola/data/samplesite/stories/bootstrap-demo.rst
+++ b/nikola/data/samplesite/stories/bootstrap-demo.rst
@@ -2,7 +2,7 @@
.. slug: bootstrap-demo
.. date: 2012-03-30 23:00:00 UTC-03:00
.. tags: bootstrap, demo
-.. link: http://getnikola.com
+.. link: https://getnikola.com
.. description:
diff --git a/nikola/data/samplesite/stories/upgrading-to-v6.txt b/nikola/data/samplesite/stories/upgrading-to-v6.txt
deleted file mode 120000
index b514b70..0000000
--- a/nikola/data/samplesite/stories/upgrading-to-v6.txt
+++ /dev/null
@@ -1 +0,0 @@
-../../../../docs/upgrading-to-v6.txt \ No newline at end of file
diff --git a/nikola/data/symlink-test-link.txt b/nikola/data/symlink-test-link.txt
new file mode 120000
index 0000000..bd0f6c1
--- /dev/null
+++ b/nikola/data/symlink-test-link.txt
@@ -0,0 +1 @@
+symlink-test-orig.txt \ No newline at end of file
diff --git a/nikola/data/symlink-test-orig.txt b/nikola/data/symlink-test-orig.txt
new file mode 100644
index 0000000..6242bf3
--- /dev/null
+++ b/nikola/data/symlink-test-orig.txt
@@ -0,0 +1,4 @@
+NIKOLA_SYMLINKS=OK
+
+This is a test file for symlink detection, used to test compatibility with
+Microsoft Windows.
diff --git a/nikola/data/symlinked.txt b/nikola/data/symlinked.txt
index 5a08781..aae8ea1 100644
--- a/nikola/data/symlinked.txt
+++ b/nikola/data/symlinked.txt
@@ -5,29 +5,21 @@ docs/sphinx/internals.txt
docs/sphinx/manual.txt
docs/sphinx/social_buttons.txt
docs/sphinx/theming.txt
-docs/sphinx/upgrading-to-v6.txt
nikola/data/samplesite/stories/creating-a-theme.rst
nikola/data/samplesite/stories/extending.txt
nikola/data/samplesite/stories/internals.txt
nikola/data/samplesite/stories/manual.rst
nikola/data/samplesite/stories/social_buttons.txt
nikola/data/samplesite/stories/theming.rst
-nikola/data/samplesite/stories/upgrading-to-v6.txt
+nikola/data/symlink-test-link.txt
+nikola/data/themes/base/assets/js/moment-with-locales.min.js
nikola/data/themes/base/messages/messages_cz.py
-nikola/data/themes/bootstrap-jinja/assets/css/bootstrap-responsive.css
-nikola/data/themes/bootstrap-jinja/assets/css/bootstrap-responsive.min.css
-nikola/data/themes/bootstrap-jinja/assets/css/bootstrap.css
-nikola/data/themes/bootstrap-jinja/assets/css/bootstrap.min.css
nikola/data/themes/bootstrap-jinja/assets/css/colorbox.css
nikola/data/themes/bootstrap-jinja/assets/css/images/controls.png
nikola/data/themes/bootstrap-jinja/assets/css/images/loading.gif
-nikola/data/themes/bootstrap-jinja/assets/css/theme.css
-nikola/data/themes/bootstrap-jinja/assets/img/glyphicons-halflings-white.png
-nikola/data/themes/bootstrap-jinja/assets/img/glyphicons-halflings.png
-nikola/data/themes/bootstrap-jinja/assets/js/bootstrap.js
-nikola/data/themes/bootstrap-jinja/assets/js/bootstrap.min.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js
+nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js
@@ -52,7 +44,7 @@ nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js
-nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js
+nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js
@@ -63,9 +55,9 @@ nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
-nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js
nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox-min.js
nikola/data/themes/bootstrap-jinja/assets/js/jquery.colorbox.js
+nikola/data/themes/bootstrap-jinja/assets/js/jquery.js
nikola/data/themes/bootstrap-jinja/assets/js/jquery.min.js
nikola/data/themes/bootstrap-jinja/assets/js/jquery.min.map
nikola/data/themes/bootstrap-jinja/bundles
@@ -74,6 +66,7 @@ nikola/data/themes/bootstrap/assets/css/images/controls.png
nikola/data/themes/bootstrap/assets/css/images/loading.gif
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ar.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bg.js
+nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ca.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-cs.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-da.js
@@ -98,7 +91,7 @@ nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-my.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-nl.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-no.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pl.js
-nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js
+nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ro.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-ru.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-si.js
@@ -111,6 +104,7 @@ nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
nikola/data/themes/bootstrap/assets/js/jquery.colorbox-min.js
nikola/data/themes/bootstrap/assets/js/jquery.colorbox.js
+nikola/data/themes/bootstrap/assets/js/jquery.js
nikola/data/themes/bootstrap/assets/js/jquery.min.js
nikola/data/themes/bootstrap/assets/js/jquery.min.map
nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css
@@ -119,21 +113,11 @@ nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css
nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css
nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map
nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css
-nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png
-nikola/data/themes/bootstrap3-jinja/assets/css/rst.css
-nikola/data/themes/bootstrap3-jinja/assets/css/theme.css
nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot
nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg
nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf
nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff
+nikola/data/themes/bootstrap3-jinja/assets/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/bundles
@@ -147,5 +131,7 @@ 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/plugins/command/auto/livereload.js
diff --git a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl
new file mode 100644
index 0000000..565732c
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl
@@ -0,0 +1,13 @@
+{# -*- coding: utf-8 -*- #}
+{% extends 'index.tmpl' %}
+
+{% block extra_head %}
+ {{ super() }}
+ {% if translations|length > 1 and generate_atom %}
+ {% for language in 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 %}
+{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/base.tmpl b/nikola/data/themes/base-jinja/templates/base.tmpl
index 3768b9e..00ba9d7 100644
--- a/nikola/data/themes/base-jinja/templates/base.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base.tmpl
@@ -14,7 +14,7 @@
<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
<div id="container">
{{ header.html_header() }}
- <main id="content">
+ <main id="content" role="main">
{% block content %}{% endblock %}
</main>
{{ footer.html_footer() }}
diff --git a/nikola/data/themes/base-jinja/templates/base_header.tmpl b/nikola/data/themes/base-jinja/templates/base_header.tmpl
index 7947f68..9f79e72 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('/') }}" title="{{ blog_title }}" rel="home">
+ <h1 id="brand"><a href="{{ abs_link(_link("root", None, lang)) }}" title="{{ blog_title }}" rel="home">
{% if logo_url %}
<img src="{{ logo_url }}" alt="{{ blog_title }}" id="logo">
{% endif %}
@@ -36,7 +36,7 @@
<ul>
{% for suburl, text in url %}
{% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</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,7 +44,7 @@
</ul>
{% else %}
{% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</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 %}
diff --git a/nikola/data/themes/base-jinja/templates/base_helper.tmpl b/nikola/data/themes/base-jinja/templates/base_helper.tmpl
index bd5d025..baa6810 100644
--- a/nikola/data/themes/base-jinja/templates/base_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base_helper.tmpl
@@ -2,28 +2,24 @@
{% macro html_headstart() %}
<!DOCTYPE html>
-<html
-
-{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook') %}
+<html \
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#
+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 %}
-
{% if is_rtl %}
dir="rtl"
{% endif %}
lang="{{ lang }}">
- <head>
+<head>
<meta charset="utf-8">
{% if description %}
<meta name="description" content="{{ description }}">
@@ -83,6 +79,10 @@ lang="{{ lang }}">
<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_feedlinks() %}
@@ -97,13 +97,22 @@ lang="{{ lang }}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}">
{% endif %}
{% endif %}
+ {% if generate_atom %}
+ {% if translations|length > 1 %}
+ {% for language in translations %}
+ <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}">
+ {% endfor %}
+ {% else %}
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}">
+ {% endif %}
+ {% endif %}
{% endmacro %}
{% macro html_translations() %}
<ul class="translations">
{% for langname in translations.keys() %}
{% if langname != lang %}
- <li><a href="{{ _link("index", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
+ <li><a href="{{ abs_link(_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_disqus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
index 8288bd4..0d40b0b 100644
--- a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
@@ -1,10 +1,4 @@
{# -*- coding: utf-8 -*- #}
-<%!
- import json
- translations = {
- 'es': 'es_ES',
- }
-%>
{% macro comment_form(url, title, identifier) %}
{% if comment_system_id %}
@@ -17,7 +11,11 @@
disqus_title={{ title|tojson }},
disqus_identifier="{{ identifier }}",
disqus_config = function () {
- this.language = "{{ translations.get(lang, lang) }}";
+ {% if lang == 'es' %}
+ this.language = "es_ES";
+ {% else %}
+ this.language = "{{ lang }}";
+ {% endif %}
};
(function() {
var dsq = document.createElement('script'); dsq.async = true;
diff --git a/nikola/data/themes/base-jinja/templates/crumbs.tmpl b/nikola/data/themes/base-jinja/templates/crumbs.tmpl
index eede9c2..970d509 100644
--- a/nikola/data/themes/base-jinja/templates/crumbs.tmpl
+++ b/nikola/data/themes/base-jinja/templates/crumbs.tmpl
@@ -5,7 +5,13 @@
<nav class="breadcrumbs">
<ul class="breadcrumb">
{% for link, text in crumbs %}
- <li><a href="{{ link }}">{{ text }}</a></li>
+ {% if text != index_file %}
+ {% if link == '#' %}
+ <li>{{ text.rsplit('.html', 1)[0] }}</li>
+ {% else %}
+ <li><a href="{{ link }}">{{ text }}</a></li>
+ {% endif %}
+ {% endif %}
{% endfor %}
</ul>
</nav>
diff --git a/nikola/data/themes/base-jinja/templates/gallery.tmpl b/nikola/data/themes/base-jinja/templates/gallery.tmpl
index 86eea12..0a53ebe 100644
--- a/nikola/data/themes/base-jinja/templates/gallery.tmpl
+++ b/nikola/data/themes/base-jinja/templates/gallery.tmpl
@@ -7,7 +7,7 @@
{% block content %}
{{ ui.bar(crumbs) }}
{% if title %}
- <h1>{{ title }}</h1>
+ <h1>{{ title|e }}</h1>
{% endif %}
{% if post %}
<p>
@@ -34,3 +34,8 @@
{{ comments.comment_form(None, permalink, title) }}
{% endif %}
{% endblock %}
+
+{% block extra_head %}
+{{ super() }}
+<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
+{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/index.tmpl b/nikola/data/themes/base-jinja/templates/index.tmpl
index 206fc34..fd9fcf4 100644
--- a/nikola/data/themes/base-jinja/templates/index.tmpl
+++ b/nikola/data/themes/base-jinja/templates/index.tmpl
@@ -3,15 +3,23 @@
{% import 'comments_helper.tmpl' as comments 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 %}
+{% endblock %}
+
{% block content %}
+{% block content_header %}{% endblock %}
<div class="postindex">
{% for post in posts %}
<article class="h-entry post-{{ post.meta('type') }}">
<header>
- <h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title() }}</h1></a>
+ <h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title()|e }}</a></h1>
<div class="metadata">
<p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p>
- <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p>
+ <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></a></p>
{% if not post.meta('nocomments') and site_has_comments %}
<p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
{% endif %}
diff --git a/nikola/data/themes/base-jinja/templates/list_post.tmpl b/nikola/data/themes/base-jinja/templates/list_post.tmpl
index b90f237..166d8c4 100644
--- a/nikola/data/themes/base-jinja/templates/list_post.tmpl
+++ b/nikola/data/themes/base-jinja/templates/list_post.tmpl
@@ -9,7 +9,7 @@
{% if posts %}
<ul class="postlist">
{% for post in posts %}
- <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title() }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></li>
+ <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li>
{% endfor %}
</ul>
{% else %}
diff --git a/nikola/data/themes/base-jinja/templates/post.tmpl b/nikola/data/themes/base-jinja/templates/post.tmpl
index e6dc97b..28da35e 100644
--- a/nikola/data/themes/base-jinja/templates/post.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post.tmpl
@@ -10,14 +10,17 @@
<meta name="keywords" content="{{ post.meta('keywords')|e }}">
{% endif %}
{% if post.description() %}
- <meta name="description" itemprop="description" content="{{ post.description() }}">
+ <meta name="description" content="{{ post.description() }}">
{% endif %}
<meta name="author" content="{{ post.author() }}">
{% if post.prev_post %}
- <link rel="prev" href="{{ post.prev_post.permalink() }}" title="{{ post.prev_post.title() }}" type="text/html">
+ <link rel="prev" href="{{ post.prev_post.permalink() }}" title="{{ post.prev_post.title()|e }}" type="text/html">
{% endif %}
{% if post.next_post %}
- <link rel="next" href="{{ post.next_post.permalink() }}" title="{{ post.next_post.title() }}" type="text/html">
+ <link rel="next" href="{{ post.next_post.permalink() }}" title="{{ post.next_post.title()|e }}" type="text/html">
+ {% endif %}
+ {% if post.is_draft %}
+ <meta name="robots" content="noindex">
{% endif %}
{{ helper.open_graph_metadata(post) }}
{{ helper.twitter_card_information(post) }}
@@ -37,7 +40,7 @@
</nav>
</aside>
{% if not post.meta('nocomments') and site_has_comments %}
- <section class="comments">
+ <section class="comments hidden-print">
<h2>{{ messages("Comments") }}</h2>
{{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }}
</section>
diff --git a/nikola/data/themes/base-jinja/templates/post_header.tmpl b/nikola/data/themes/base-jinja/templates/post_header.tmpl
index 0ed40b9..00b6210 100644
--- a/nikola/data/themes/base-jinja/templates/post_header.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post_header.tmpl
@@ -4,12 +4,12 @@
{% macro html_title() %}
{% if title and not post.meta('hidetitle') %}
- <h1 class="p-name entry-title" itemprop="headline name"><a href="{{ post.permalink() }}" class="u-url">{{ title|e }}</a></h1>
+ <h1 class="p-name entry-title" itemprop="headline name"><a href="{{ post.permalink() }}" class="u-url">{{ post.title()|e }}</a></h1>
{% endif %}
{% endmacro %}
{% macro html_translations(post) %}
- {% if translations|length > 1 %}
+ {% if post.translated_to|length > 1 %}
<div class="metadata posttranslations translations">
<h3 class="posttranslations-intro">{{ messages("Also available in:") }}</h3>
{% for langname in translations.keys() %}
@@ -32,13 +32,13 @@
{{ html_title() }}
<div class="metadata">
<p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p>
- <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p>
+ <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></a></p>
{% if not post.meta('nocomments') and site_has_comments %}
<p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
{% endif %}
{{ html_sourcelink() }}
{% if post.meta('link') %}
- <p><a href='{{ post.meta('link') }}'>{{ messages("Original site") }}</a></p>
+ <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() }}">
diff --git a/nikola/data/themes/base-jinja/templates/post_helper.tmpl b/nikola/data/themes/base-jinja/templates/post_helper.tmpl
index 541cd31..ae0206b 100644
--- a/nikola/data/themes/base-jinja/templates/post_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post_helper.tmpl
@@ -14,7 +14,9 @@
{% if post.tags %}
<ul itemprop="keywords" class="tags">
{% for tag in post.tags %}
- <li><a class="tag p-category" href="{{ _link('tag', tag) }}" rel="tag">{{ tag }}</a></li>
+ {% if tag not in hidden_tags %}
+ <li><a class="tag p-category" href="{{ _link('tag', tag) }}" rel="tag">{{ tag }}</a></li>
+ {% endif %}
{% endfor %}
</ul>
{% endif %}
@@ -22,15 +24,15 @@
{% macro html_pager(post) %}
{% if post.prev_post or post.next_post %}
- <ul class="pager">
+ <ul class="pager hidden-print">
{% if post.prev_post %}
<li class="previous">
- <a href="{{ post.prev_post.permalink() }}" rel="prev" title="{{ post.prev_post.title() }}">{{ messages("Previous post") }}</a>
+ <a href="{{ post.prev_post.permalink() }}" rel="prev" title="{{ post.prev_post.title()|e }}">{{ messages("Previous post") }}</a>
</li>
{% endif %}
{% if post.next_post %}
<li class="next">
- <a href="{{ post.next_post.permalink() }}" rel="next" title="{{ post.next_post.title() }}">{{ messages("Next post") }}</a>
+ <a href="{{ post.next_post.permalink() }}" rel="next" title="{{ post.next_post.title()|e }}">{{ messages("Next post") }}</a>
</li>
{% endif %}
</ul>
@@ -39,15 +41,30 @@
{% macro open_graph_metadata(post) %}
{% if use_open_graph %}
- <meta name="og:title" content="{{ post.title()[:70]|e }}">
- <meta name="og:url" content="{{ abs_link(permalink) }}">
+ <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 name="og:description" content="{{ post.description()[:200]|e }}">
+ <meta property="og:description" content="{{ post.description()[:200]|e }}">
{% else %}
- <meta name="og:description" content="{{ post.text(strip_html=True)[:200]|e }}">
+ <meta property="og:description" content="{{ post.text(strip_html=True)[:200]|e }}">
+ {% endif %}
+ {% if post.previewimage %}
+ <meta property="og:image" content="{{ url_replacer(permalink, post.previewimage, lang, 'absolute') }}">
+ {% 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() }}"> #}
+{# %endif #}
+ {% if post.date.isoformat() %}
+ <meta property="article:published_time" content="{{ post.date.isoformat() }}">
+ {% endif %}
+ {% if post.tags %}
+ {% for tag in post.tags %}
+ <meta property="article:tag" content="{{ tag }}">
+ {% endfor %}
{% endif %}
- <meta name="og:site_name" content="{{ blog_title|e }}">
- <meta name="og:type" content="article">
{% endif %}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl b/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl
index ceaec3f..92c1b7d 100644
--- a/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post_list_directive.tmpl
@@ -8,7 +8,7 @@
<li class="post-list-item">
{{ post.formatted_date(date_format) }}
&nbsp;
- <a href="{{ post.permalink(lang) }}">{{ post.title(lang) }}</a>
+ <a href="{{ post.permalink(lang) }}">{{ post.title(lang)|e }}</a>
</li>
{% endfor %}
</ul>
diff --git a/nikola/data/themes/base-jinja/templates/tag.tmpl b/nikola/data/themes/base-jinja/templates/tag.tmpl
index 84f9e68..765c122 100644
--- a/nikola/data/themes/base-jinja/templates/tag.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tag.tmpl
@@ -16,7 +16,18 @@
{% block content %}
<article class="tagpage">
<header>
- <h1>{{ title }}</h1>
+ <h1>{{ title|e }}</h1>
+ {% if description %}
+ <p>{{ description }}</p>
+ {% endif %}
+ {% if subcategories %}
+ {{ messages('Subcategories:') }}
+ <ul>
+ {% for name, link in subcategories %}
+ <li><a href="{{ link }}">{{ name }}</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
<div class="metadata">
{% if translations|length > 1 and generate_rss %}
{% for language in translations %}
@@ -32,7 +43,7 @@
{% if posts %}
<ul class="postlist">
{% for post in posts %}
- <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title() }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></li>
+ <li><a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a> <time class="listdate" datetime="{{ post.date.isoformat() }}" title="{{ post.formatted_date(date_format) }}">{{ post.formatted_date(date_format) }}</time></li>
{% endfor %}
</ul>
{% endif %}
diff --git a/nikola/data/themes/base-jinja/templates/tagindex.tmpl b/nikola/data/themes/base-jinja/templates/tagindex.tmpl
index af0a992..ee7d8b0 100644
--- a/nikola/data/themes/base-jinja/templates/tagindex.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tagindex.tmpl
@@ -1,2 +1,24 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}
+
+{% block content_header %}
+ {% if subcategories %}
+ {{ messages('Subcategories:') }}
+ <ul>
+ {% for name, link in subcategories %}
+ <li><a href="{{ link }}">{{ name }}</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endblock %}
+
+{% block extra_head %}
+ {{ super() }}
+ {% if translations|length > 1 and generate_atom %}
+ {% for language in translations %}
+ <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag }} section ({{ language }})" href="{{ _link(kind + "_atom", tag, language) }}">
+ {% endfor %}
+ {% elif generate_atom %}
+ <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag }} section" href="{{ _link("tag" + "_atom", tag) }}">
+ {% endif %}
+{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/tags.tmpl b/nikola/data/themes/base-jinja/templates/tags.tmpl
index 7bcb7b2..4605fc9 100644
--- a/nikola/data/themes/base-jinja/templates/tags.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tags.tmpl
@@ -7,14 +7,24 @@
<h1>{{ title }}</h1>
</header>
{% if cat_items %}
- <h2>{{ messages("Categories") }}</h2>
- <ul class="postlist">
- {% for text, link in cat_items %}
- {% if text %}
- <li><a class="reference" href="{{ link }}">{{ text }}</a></li>
+ {% if items %}
+ <h2>{{ messages("Categories") }}</h2>
+ {% endif %}
+ {% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
+ {% for i in range(indent_change_before) %}
+ <ul class="postlist">
+ {% endfor %}
+ <li><a class="reference" href="{{ link }}">{{ text }}</a>
+ {% if indent_change_after <= 0 %}
+ </li>
{% endif %}
+ {% for i in range(-indent_change_after) %}
+ </ul>
+ {% if i + 1 < indent_levels|length %}
+ </li>
+ {% endif %}
+ {% endfor %}
{% endfor %}
- </ul>
{% if items %}
<h2>{{ messages("Tags") }}</h2>
{% endif %}
@@ -22,7 +32,9 @@
{% if items %}
<ul class="postlist">
{% for text, link in items %}
- <li><a class="reference listtitle" href="{{ link }}">{{ text }}</a></li>
+ {% if text not in hidden_tags %}
+ <li><a class="reference listtitle" href="{{ link }}">{{ text }}</a></li>
+ {% endif %}
{% endfor %}
</ul>
{% endif %}
diff --git a/nikola/data/themes/base/assets/css/nikola_ipython.css b/nikola/data/themes/base/assets/css/nikola_ipython.css
new file mode 100644
index 0000000..5ae5189
--- /dev/null
+++ b/nikola/data/themes/base/assets/css/nikola_ipython.css
@@ -0,0 +1,116 @@
+div.prompt {
+ padding: 0.6em;
+ font-size: 13px;
+ background-color: #E9E9E9;
+ margin-right: 1em;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+div.output_prompt {
+ /* 5px right shift to account for margin in parent container */
+ margin: 0 5px 0 0px;
+}
+
+div.output_area pre {
+ font-size: 13px;
+}
+
+div.text_cell_render {
+ padding: 0px;
+ color: #333333;
+}
+
+.rendered_html p {
+ text-align: left;
+}
+
+.rendered_html ul {
+ margin: 0 0 12px 25px;
+}
+
+.rendered_html :visited {
+ text-decoration: none;
+}
+
+.rendered_html :link {
+ text-decoration: none;
+}
+
+.rendered_html pre, .rendered_html code {
+ background-color: #DDDDDD;
+ 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/rst.css b/nikola/data/themes/base/assets/css/rst.css
index 784308b..6e6de97 100644
--- a/nikola/data/themes/base/assets/css/rst.css
+++ b/nikola/data/themes/base/assets/css/rst.css
@@ -253,7 +253,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
-pre.code, code { background-color: #eeeeee }
+pre.code, code { background-color: #eeeeee; }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
diff --git a/nikola/data/themes/base/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css
index 18b93db..3cb8628 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 Daniel Aleksandersen and others.
+ Copyright © 2014-2015 Daniel Aleksandersen and others.
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -33,11 +33,6 @@ body {
line-height: 1.4;
padding: 1em;
}
-@media print {
- body {
- font-family: Garamond, serif;
- }
-}
#container {
margin: 1em auto;
@@ -283,3 +278,111 @@ img {
overflow: visible;
clip: auto;
}
+
+pre.code, code {
+ white-space: pre;
+ word-wrap: normal;
+ overflow: auto;
+}
+
+/* SOURCE: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
+@media print {
+ *,
+ *:before,
+ *:after {
+ background: transparent !important;
+ color: #000 !important; /* Black prints faster: http://www.sanbeiji.com/archives/953 */
+ box-shadow: none !important;
+ text-shadow: none !important;
+ font-family: Garamond, Junicode, serif;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ a,
+ a:visited {
+ text-decoration: underline;
+ }
+
+ a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ abbr[title]:after {
+ content: " (" attr(title) ")";
+ }
+
+ /*
+ * Don't show links that are fragment identifiers,
+ * or use the `javascript:` pseudo protocol
+ */
+
+ a[href^="#"]:after,
+ a[href^="javascript:"]:after {
+ content: "";
+ }
+
+ pre,
+ blockquote {
+ border: 1px solid #999;
+ page-break-inside: avoid;
+ }
+
+ /*
+ * Printing Tables:
+ * http://css-discuss.incutio.com/wiki/Printing_Tables
+ */
+
+ thead {
+ display: table-header-group;
+ }
+
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+
+ img {
+ max-width: 100% !important;
+ }
+
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+
+ .hidden-print {
+ display: none !important;
+ }
+
+ article .entry-title a[href]:after,
+ article .metadata a[href]:after,
+ article .tags a[href]:after {
+ content: "";
+ }
+
+ article .metadata .sourceline {
+ display: none;
+ }
+
+ article .metadata .linkline a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ #header {
+ display: none;
+ }
+
+ .postpromonav {
+ padding: 0;
+ }
+}
diff --git a/nikola/data/themes/base/assets/js/fancydates.js b/nikola/data/themes/base/assets/js/fancydates.js
new file mode 100644
index 0000000..d13b11b
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/fancydates.js
@@ -0,0 +1,20 @@
+function fancydates(fanciness, date_format) {
+ if (fanciness == 0) {
+ return;
+ }
+
+ dates = $('time.published.dt-published');
+
+ i = 0;
+ 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);
+ } else {
+ o = d.fromNow();
+ }
+ dates[i].innerHTML = o;
+ }
+}
diff --git a/nikola/data/themes/base/assets/js/mathjax.js b/nikola/data/themes/base/assets/js/mathjax.js
index 5e14369..6ef2dd7 100644
--- a/nikola/data/themes/base/assets/js/mathjax.js
+++ b/nikola/data/themes/base/assets/js/mathjax.js
@@ -5,12 +5,7 @@
window.onload = function () {
setTimeout(function () {
var script = document.createElement("script");
- if (location.protocol == 'https:') {
- scriptbase = "https://c328740.ssl.cf1.rackcdn.com/";
- } else {
- scriptbase = "http://cdn.mathjax.org/";
- }
- script.src = scriptbase + "mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
+ script.src = "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
document.getElementsByTagName("body")[0].appendChild(script);
},1)
}
diff --git a/nikola/data/themes/base/assets/xml/atom.xsl b/nikola/data/themes/base/assets/xml/atom.xsl
new file mode 100644
index 0000000..cc052e0
--- /dev/null
+++ b/nikola/data/themes/base/assets/xml/atom.xsl
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
+<xsl:output method="xml"/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+<head>
+<meta charset="UTF-8"/>
+<meta name="viewport" content="width=device-width"/>
+<title><xsl:value-of select="feed/title"/> (Atom feed)</title>
+<style><![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]></style>
+</head>
+<body>
+<h1><xsl:value-of select="feed/title"/> (Atom feed)</h1>
+<p>This is an Atom feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader. New to feeds? <a href="https://duckduckgo.com/?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
+<p>
+<label for="address">Atom feed address:</label>
+<input><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="feed/link[@rel='self']/@href"/></xsl:attribute></input>
+</p>
+<p>Preview of the feed’s current headlines:</p>
+<ol>
+<xsl:for-each select="feed/entry">
+<li><h2><a><xsl:attribute name="href"><xsl:value-of select="link[@rel='alternate']/@href"/></xsl:attribute><xsl:value-of select="title"/></a></h2></li>
+</xsl:for-each>
+</ol>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>
diff --git a/nikola/data/themes/base/assets/xml/rss.xsl b/nikola/data/themes/base/assets/xml/rss.xsl
new file mode 100644
index 0000000..ee72301
--- /dev/null
+++ b/nikola/data/themes/base/assets/xml/rss.xsl
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
+<xsl:output method="xml"/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+<head>
+<meta charset="UTF-8"/>
+<meta name="viewport" content="width=device-width"/>
+<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
+<style><![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]></style>
+</head>
+<body>
+<h1><xsl:value-of select="rss/channel/title"/> (RSS)</h1>
+<p>This is an <abbr title="Really Simple Syndication">RSS</abbr> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader. New to feeds? <a href="https://duckduckgo.com/?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
+<p>
+<label for="address">RSS address:</label>
+<input><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="rss/channel/atom:link[@rel='self']/@href"/></xsl:attribute></input>
+</p>
+<p>Preview of the feed’s current headlines:</p>
+<ol>
+<xsl:for-each select="rss/channel/item">
+<li><h2><a><xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute><xsl:value-of select="title"/></a></h2></li>
+</xsl:for-each>
+</ol>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>
diff --git a/nikola/data/themes/base/messages/messages_ar.py b/nikola/data/themes/base/messages/messages_ar.py
index f7ba16a..07c38b2 100644
--- a/nikola/data/themes/base/messages/messages_ar.py
+++ b/nikola/data/themes/base/messages/messages_ar.py
@@ -3,32 +3,37 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
- "Also available in:": "",
- "Archive": "",
- "Categories": "الأصناف",
+ "(active)": "",
+ "Also available in:": "أيضا متوفر في:",
+ "Archive": "الأرشيف",
+ "Categories": "فئات",
"Comments": "التّعليقات",
"LANGUAGE": "العربيّة",
- "Languages:": "",
+ "Languages:": "اللغات",
"More posts about %s": "المزيد من المقالات حول %s",
- "Newer posts": "",
- "Next post": "",
- "No posts found.": "",
- "Nothing found.": "",
- "Older posts": "",
- "Original site": "",
- "Posted:": "",
- "Posts about %s": "",
- "Posts for year %s": "",
+ "Newer posts": "مقالات أحدث",
+ "Next post": "المقالة التالية",
+ "No posts found.": "لم يوجد مقالات.",
+ "Nothing found.": "لم يوجد شيء.",
+ "Older posts": "مقالات أقدم",
+ "Original site": "الموقع الأصلي",
+ "Posted:": "نشر:",
+ "Posts about %s": "مقالات عن s%",
+ "Posts for year %s": "مقالات سنة s%",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "",
- "Previous post": "",
- "Publication date": "",
+ "Previous post": "المقالة السابقة",
+ "Publication date": "تاريخ النشر",
"RSS feed": "",
- "Read in English": "",
+ "Read in English": "اقرأ بالعربية",
"Read more": "قراءة المزيد",
- "Skip to main content": "",
- "Source": "",
- "Tags and Categories": "",
- "Tags": "",
- "old posts, page %d": "",
- "page %d": "",
+ "Skip to main content": "انتقل إلى المحتوى الرئيسي",
+ "Source": "المصدر",
+ "Subcategories:": "",
+ "Tags and Categories": "تصنيفات و فئات",
+ "Tags": "تصنيفات",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "مقالات قديمة, صفحة d%",
+ "page %d": "صفحة d%",
}
diff --git a/nikola/data/themes/base/messages/messages_az.py b/nikola/data/themes/base/messages/messages_az.py
new file mode 100644
index 0000000..76c15fc
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_az.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "%d dəqiqəlik oxuma",
+ "(active)": "",
+ "Also available in:": "Həmçinin mövcuddur:",
+ "Archive": "Arxiv",
+ "Categories": "Kateqoriyalar",
+ "Comments": "Şərhlər",
+ "LANGUAGE": "Azərbaycan dili",
+ "Languages:": "Dillər:",
+ "More posts about %s": "%s ilə bağlı digər yazılar",
+ "Newer posts": "Yeni yazılar",
+ "Next post": "Növbəti yazı",
+ "No posts found.": "Heç bir yazı tapılmadı",
+ "Nothing found.": "Heç nə tapılmadı",
+ "Older posts": "Köhnə yazılar",
+ "Original site": "Original sayt",
+ "Posted:": "yazılma tarixi:",
+ "Posts about %s": "%s ilə bağlı 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",
+ "Previous post": "Əvvəlki yazı",
+ "Publication date": "Buraxılış tarixi",
+ "RSS feed": "RSS",
+ "Read in English": "Azərbaycan dilində oxu",
+ "Read more": "Davamı",
+ "Skip to main content": "Əsas mövzuya keç",
+ "Source": "Mənbə",
+ "Subcategories:": "",
+ "Tags and Categories": "Teqlər və Kateqoriyalar",
+ "Tags": "Teqlər",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "köhnə yazılar, səhifə %s",
+ "page %d": "səhifə %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py
index 28adb77..96ac878 100644
--- a/nikola/data/themes/base/messages/messages_bg.py
+++ b/nikola/data/themes/base/messages/messages_bg.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Също достъпно в:",
"Archive": "Архив",
"Categories": "Категории",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Публиковано:",
"Posts about %s": "Публикации относно %s",
"Posts for year %s": "Публикации за %s година",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Публикации за {month} {year}",
"Previous post": "Предишна публикация",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Прочети още",
"Skip to main content": "",
"Source": "Source",
+ "Subcategories:": "",
"Tags and Categories": "Тагове и Категории",
"Tags": "Тагове",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "стари публикации, страница %d",
"page %d": "страница %d",
}
diff --git a/nikola/data/themes/base/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py
index 58c8577..f45b1a6 100644
--- a/nikola/data/themes/base/messages/messages_ca.py
+++ b/nikola/data/themes/base/messages/messages_ca.py
@@ -2,33 +2,38 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "% min recordar per a llegir",
+ "(active)": "",
"Also available in:": "També disponibles en:",
"Archive": "Arxiu",
- "Categories": "",
- "Comments": "",
+ "Categories": "Categories",
+ "Comments": "Comentaris",
"LANGUAGE": "Català",
- "Languages:": "",
+ "Languages:": "Llenguatges:",
"More posts about %s": "Més entrades sobre %s",
"Newer posts": "Entrades posteriors",
"Next post": "Entrada següent",
- "No posts found.": "",
- "Nothing found.": "",
+ "No posts found.": "Publicació no trobada",
+ "Nothing found.": "No trobat",
"Older posts": "Entrades anteriors",
"Original site": "Lloc original",
"Posted:": "Publicat:",
"Posts about %s": "Entrades sobre %s",
"Posts for year %s": "Entrades de l'any %s",
- "Posts for {month} {year}": "",
+ "Posts for {month} {day}, {year}": "",
+ "Posts for {month} {year}": "Publicat en {month} {year}",
"Previous post": "Entrada anterior",
- "Publication date": "",
- "RSS feed": "",
+ "Publication date": "Data de publicació",
+ "RSS feed": "Feed RSS",
"Read in English": "Llegeix-ho en català",
"Read more": "Llegeix-ne més",
- "Skip to main content": "",
+ "Skip to main content": "Vés al comentari principal",
"Source": "Codi",
- "Tags and Categories": "",
+ "Subcategories:": "",
+ "Tags and Categories": "Etiquetes i Categories",
"Tags": "Etiquetes",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "entrades antigues, pàgina %d",
"page %d": "pàgina %d",
}
diff --git a/nikola/data/themes/base/messages/messages_cs.py b/nikola/data/themes/base/messages/messages_cs.py
index 548c6d7..6e1bdd2 100644
--- a/nikola/data/themes/base/messages/messages_cs.py
+++ b/nikola/data/themes/base/messages/messages_cs.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d min zbývajících",
+ "(active)": "",
"Also available in:": "Dostupné také v",
"Archive": "Archiv",
"Categories": "Kategorie",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Zveřejněno:",
"Posts about %s": "Příspěvky o %s",
"Posts for year %s": "Příspěvky v roce %s",
+ "Posts for {month} {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",
"Publication date": "Datum zveřejnění",
"RSS feed": "RSS zdroj",
"Read in English": "Číst v češtině",
"Read more": "Číst dál",
- "Skip to main content": "",
+ "Skip to main content": "Přeskočit na hlavní obsah",
"Source": "Zdroj",
+ "Subcategories:": "",
"Tags and Categories": "Štítky a kategorie",
"Tags": "Štítky",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "staré příspěvky, strana %d",
"page %d": "strana %d",
}
diff --git a/nikola/data/themes/base/messages/messages_da.py b/nikola/data/themes/base/messages/messages_da.py
index c5c82ee..73b71cb 100644
--- a/nikola/data/themes/base/messages/messages_da.py
+++ b/nikola/data/themes/base/messages/messages_da.py
@@ -2,33 +2,38 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
- "Also available in:": "",
- "Archive": "",
- "Categories": "",
- "Comments": "",
- "LANGUAGE": "",
- "Languages:": "",
- "More posts about %s": "",
- "Newer posts": "",
- "Next post": "",
- "No posts found.": "",
- "Nothing found.": "",
- "Older posts": "",
- "Original site": "",
- "Posted:": "",
- "Posts about %s": "",
- "Posts for year %s": "",
- "Posts for {month} {year}": "",
- "Previous post": "",
- "Publication date": "",
- "RSS feed": "",
- "Read in English": "",
- "Read more": "",
- "Skip to main content": "",
- "Source": "",
- "Tags and Categories": "",
- "Tags": "",
- "old posts, page %d": "",
- "page %d": "",
+ "%d min remaining to read": "%d min. tilbage at læse",
+ "(active)": "",
+ "Also available in:": "Fås også i:",
+ "Archive": "Arkiv",
+ "Categories": "Kategorier",
+ "Comments": "Kommentarer",
+ "LANGUAGE": "Dansk",
+ "Languages:": "Sprog:",
+ "More posts about %s": "Yderligere indlæg om %s",
+ "Newer posts": "Nyere indlæg",
+ "Next post": "Næste indlæg",
+ "No posts found.": "Søgningen gav ingen resultater.",
+ "Nothing found.": "Søgningen gav ingen resultater.",
+ "Older posts": "Ældre indlæg",
+ "Original site": "Oprindeligt hjemmeside",
+ "Posted:": "Opslået:",
+ "Posts about %s": "Indlæg om %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}",
+ "Previous post": "Tidligere indlæg",
+ "Publication date": "Udgivelsesdato",
+ "RSS feed": "RSS-nyhedskilde",
+ "Read in English": "Læs på dansk",
+ "Read more": "Læs mere",
+ "Skip to main content": "Hop direkte til hovedindhold",
+ "Source": "Kilde",
+ "Subcategories:": "",
+ "Tags and Categories": "Nøgleord og kategorier",
+ "Tags": "Nøgleord",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "gamle indlæg, side %d",
+ "page %d": "side %d",
}
diff --git a/nikola/data/themes/base/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py
index 6be6ad5..324dd14 100644
--- a/nikola/data/themes/base/messages/messages_de.py
+++ b/nikola/data/themes/base/messages/messages_de.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d min verbleiben zum Lesen",
+ "(active)": "",
"Also available in:": "Auch verfügbar in:",
"Archive": "Archiv",
"Categories": "Kategorien",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Veröffentlicht:",
"Posts about %s": "Einträge über %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}",
"Previous post": "Vorheriger Eintrag",
"Publication date": "Veröffentlichungsdatum",
"RSS feed": "RSS-Feed",
"Read in English": "Auf Deutsch lesen",
"Read more": "Weiterlesen",
- "Skip to main content": "",
+ "Skip to main content": "Springe zum Hauptinhalt",
"Source": "Source",
+ "Subcategories:": "Unterkategorien:",
"Tags and Categories": "Tags und Kategorien",
"Tags": "Tags",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "Ältere Einträge, Seite %d",
"page %d": "Seite %d",
}
diff --git a/nikola/data/themes/base/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py
index ce2fd89..68cf91f 100644
--- a/nikola/data/themes/base/messages/messages_el.py
+++ b/nikola/data/themes/base/messages/messages_el.py
@@ -3,32 +3,37 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Διαθέσιμο και στα:",
"Archive": "Αρχείο",
"Categories": "Κατηγορίες",
- "Comments": "",
+ "Comments": "Σχόλια",
"LANGUAGE": "Ελληνικά",
- "Languages:": "",
+ "Languages:": "Γλώσσες",
"More posts about %s": "Περισσότερες αναρτήσεις για %s",
"Newer posts": "Νεότερες αναρτήσεις",
"Next post": "Επόμενη ανάρτηση",
- "No posts found.": "",
- "Nothing found.": "",
+ "No posts found.": "Δε βρέθηκαν αναρτήσεις",
+ "Nothing found.": "Δε βρέθηκε περιεχόμενο",
"Older posts": "Παλαιότερες αναρτήσεις",
"Original site": "Ιστοσελίδα αρχικής ανάρτησης",
"Posted:": "Αναρτήθηκε:",
"Posts about %s": "Αναρτήσεις για %s",
"Posts for year %s": "Αναρτήσεις για το έτος %s",
+ "Posts for {month} {day}, {year}": "Αναρτήσεις στις {day} {month}, {year}",
"Posts for {month} {year}": "Αναρτήσεις για τον {month} του {year}",
"Previous post": "Προηγούμενη ανάρτηση",
- "Publication date": "",
+ "Publication date": "Ημερομηνία δημοσίευσης",
"RSS feed": "",
"Read in English": "Διαβάστε στα Ελληνικά",
"Read more": "Διαβάστε περισσότερα",
"Skip to main content": "",
"Source": "Πηγαίος κώδικας",
+ "Subcategories:": "",
"Tags and Categories": "Ετικέτες και κατηγορίες",
"Tags": "Ετικέτες",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "σελίδα παλαιότερων αναρτήσεων %d",
"page %d": "σελίδα %d",
}
diff --git a/nikola/data/themes/base/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py
index df04974..29af257 100644
--- a/nikola/data/themes/base/messages/messages_en.py
+++ b/nikola/data/themes/base/messages/messages_en.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d min remaining to read",
+ "(active)": "(active)",
"Also available in:": "Also available in:",
"Archive": "Archive",
"Categories": "Categories",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Posted:",
"Posts about %s": "Posts about %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}",
"Previous post": "Previous post",
"Publication date": "Publication date",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Read more",
"Skip to main content": "Skip to main content",
"Source": "Source",
+ "Subcategories:": "Subcategories:",
"Tags and Categories": "Tags and Categories",
"Tags": "Tags",
+ "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",
}
diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py
index 38b54e2..8938707 100644
--- a/nikola/data/themes/base/messages/messages_eo.py
+++ b/nikola/data/themes/base/messages/messages_eo.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Ankaŭ disponebla en:",
"Archive": "Arĥivo",
"Categories": "Kategorioj",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Skribita:",
"Posts about %s": "Artikoloj pri %s",
"Posts for year %s": "Artikoloj de la jaro %s",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Artikoloj skribitaj en {month} {year}",
"Previous post": "Antaŭa artikolo",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Legu plu",
"Skip to main content": "",
"Source": "Fonto",
+ "Subcategories:": "",
"Tags and Categories": "Etikedoj kaj Kategorioj",
"Tags": "Etikedoj",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "paĝo de malnovaj artikoloj %d",
"page %d": "paĝo %d",
}
diff --git a/nikola/data/themes/base/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py
index 67de5aa..e3f0f03 100644
--- a/nikola/data/themes/base/messages/messages_es.py
+++ b/nikola/data/themes/base/messages/messages_es.py
@@ -2,33 +2,38 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "restan %d minutos",
+ "%d min remaining to read": "quedan %d minutos de lectura",
+ "(active)": "(activo)",
"Also available in:": "También disponible en:",
"Archive": "Archivo",
"Categories": "Categorías",
"Comments": "Comentarios",
"LANGUAGE": "Español",
"Languages:": "Idiomas:",
- "More posts about %s": "Más posts sobre %s",
- "Newer posts": "Posts posteriores",
- "Next post": "Siguiente post",
- "No posts found.": "No se encontraron posts",
- "Nothing found.": "No encontrado",
- "Older posts": "Posts anteriores",
+ "More posts about %s": "Más publicaciones sobre %s",
+ "Newer posts": "Publicaciones posteriores",
+ "Next post": "Siguiente publicación",
+ "No posts found.": "No se encontraron publicaciones.",
+ "Nothing found.": "No se encontró nada.",
+ "Older posts": "Publicaciones anteriores",
"Original site": "Sitio original",
"Posted:": "Publicado:",
- "Posts about %s": "Posts sobre %s",
- "Posts for year %s": "Posts del año %s",
+ "Posts about %s": "Publicaciones sobre %s",
+ "Posts for year %s": "Publicaciones del año %s",
+ "Posts for {month} {day}, {year}": "Publicaciones de {day}, {month}, {year}",
"Posts for {month} {year}": "Posts de {month} {year}",
- "Previous post": "Post anterior",
+ "Previous post": "Publicación anterior",
"Publication date": "Fecha de publicación",
- "RSS feed": "feed RSS",
+ "RSS feed": "Canal RSS",
"Read in English": "Leer en español",
"Read more": "Leer más",
- "Skip to main content": "",
- "Source": "Código",
- "Tags and Categories": "Tags y Categorías",
- "Tags": "Tags",
- "old posts, page %d": "posts antiguos, página %d",
+ "Skip to main content": "Ir al contenido principal",
+ "Source": "Código fuente",
+ "Subcategories:": "Subcategorías:",
+ "Tags and Categories": "Etiquetas y Categorías",
+ "Tags": "Etiquetas",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "publicaciones antiguas, página %d",
"page %d": "página %d",
}
diff --git a/nikola/data/themes/base/messages/messages_et.py b/nikola/data/themes/base/messages/messages_et.py
index 3a53c2f..c67cbb2 100644
--- a/nikola/data/themes/base/messages/messages_et.py
+++ b/nikola/data/themes/base/messages/messages_et.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Saadaval ka:",
"Archive": "Arhiiv",
"Categories": "Kategooriad",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Postitatud:",
"Posts about %s": "Postitused %s kohta",
"Posts for year %s": "Postitused aastast %s",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Postitused {year} aasta kuust {month} ",
"Previous post": "Eelmine postitus",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Loe veel",
"Skip to main content": "",
"Source": "Lähtekood",
+ "Subcategories:": "",
"Tags and Categories": "Sildid ja kategooriad",
"Tags": "Märksõnad",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "vanade postituste, leht %d",
"page %d": "leht %d",
}
diff --git a/nikola/data/themes/base/messages/messages_eu.py b/nikola/data/themes/base/messages/messages_eu.py
index 6920552..1588cd6 100644
--- a/nikola/data/themes/base/messages/messages_eu.py
+++ b/nikola/data/themes/base/messages/messages_eu.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Eskuragarria hemen ere:",
"Archive": "Artxiboa",
"Categories": "Kategoriak",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Argitaratuta:",
"Posts about %s": "%s-ri buruzko postak",
"Posts for year %s": "%s. urteko postak",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "{year}ko {month}ren postak",
"Previous post": "Aurreko posta",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Irakurri gehiago",
"Skip to main content": "",
"Source": "Iturria",
+ "Subcategories:": "",
"Tags and Categories": "Etiketak eta Kategoriak",
"Tags": "Etiketak",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "Post zaharren, orria %d",
"page %d": "orria %d",
}
diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py
index 5899ec5..b69d57a 100644
--- a/nikola/data/themes/base/messages/messages_fa.py
+++ b/nikola/data/themes/base/messages/messages_fa.py
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "%d دقیقه برای خواندن باقی مانده",
+ "(active)": "",
"Also available in:": "همچنین قابل دسترس از:",
"Archive": "آرشیو",
"Categories": "دسته‌ها",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "ارسال شده:",
"Posts about %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": "تاریخ انتشار",
"RSS feed": "خوراک",
"Read in English": "به فارسی بخوانید",
"Read more": "بیشتر بخوانید",
- "Skip to main content": "",
+ "Skip to main content": "متن اصلی را نادیده بگیر",
"Source": "منبع",
+ "Subcategories:": "",
"Tags and Categories": "برچسب‌ها و دسته‌ها",
"Tags": "برچسب‌ها",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "صفحهٔ ارسال‌های قدیمی %d",
"page %d": "برگه %d",
}
diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py
index 1988e3f..bcc2680 100644
--- a/nikola/data/themes/base/messages/messages_fi.py
+++ b/nikola/data/themes/base/messages/messages_fi.py
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "%d minuuttia lukuaikaa",
+ "(active)": "",
"Also available in:": "Saatavilla myös:",
"Archive": "Arkisto",
"Categories": "Kategoriat",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Postattu:",
"Posts about %s": "Postauksia aiheesta %s",
"Posts for year %s": "Postauksia vuodelta %s",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Postauksia ajalle {month} {year}",
"Previous post": "Vanhempia postauksia",
"Publication date": "Julkaisupäivämäärä",
"RSS feed": "RSS syöte",
"Read in English": "Lue suomeksi",
"Read more": "Lue lisää",
- "Skip to main content": "",
+ "Skip to main content": "Hyppää sisältöön",
"Source": "Lähde",
+ "Subcategories:": "",
"Tags and Categories": "Tagit ja kategoriat",
"Tags": "Tagit",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "vanhoja postauksia, sivu %d",
"page %d": "sivu %d",
}
diff --git a/nikola/data/themes/base/messages/messages_fil.py b/nikola/data/themes/base/messages/messages_fil.py
new file mode 100644
index 0000000..fd26d77
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_fil.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "",
+ "(active)": "",
+ "Also available in:": "",
+ "Archive": "",
+ "Categories": "",
+ "Comments": "",
+ "LANGUAGE": "",
+ "Languages:": "",
+ "More posts about %s": "",
+ "Newer posts": "",
+ "Next post": "",
+ "No posts found.": "",
+ "Nothing found.": "",
+ "Older posts": "",
+ "Original site": "",
+ "Posted:": "",
+ "Posts about %s": "",
+ "Posts for year %s": "",
+ "Posts for {month} {day}, {year}": "",
+ "Posts for {month} {year}": "",
+ "Previous post": "",
+ "Publication date": "",
+ "RSS feed": "",
+ "Read in English": "",
+ "Read more": "",
+ "Skip to main content": "",
+ "Source": "",
+ "Subcategories:": "",
+ "Tags and Categories": "",
+ "Tags": "",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "",
+ "page %d": "",
+}
diff --git a/nikola/data/themes/base/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py
index a30aa1a..327b3f7 100644
--- a/nikola/data/themes/base/messages/messages_fr.py
+++ b/nikola/data/themes/base/messages/messages_fr.py
@@ -3,22 +3,24 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "Il reste encore %d min. de lecture",
- "Also available in:": "Egalement disponible en:",
+ "(active)": "(actif)",
+ "Also available in:": "Également disponible en:",
"Archive": "Archives",
"Categories": "Catégories",
"Comments": "Commentaires",
"LANGUAGE": "Français",
"Languages:": "Langues:",
"More posts about %s": "Plus d'articles sur %s",
- "Newer posts": "Billets récents",
+ "Newer posts": "Articles récents",
"Next post": "Article suivant",
- "No posts found.": "Pas de billets.",
+ "No posts found.": "Pas d'articles.",
"Nothing found.": "Pas de résultats.",
"Older posts": "Anciens articles",
"Original site": "Site d'origine",
"Posted:": "Publié:",
"Posts about %s": "Articles sur %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}",
"Previous post": "Article précédent",
"Publication date": "Date de publication",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Lire la suite",
"Skip to main content": "Aller au contenu principal",
"Source": "Source",
+ "Subcategories:": "Sous-catégories",
"Tags and Categories": "Étiquettes et catégories",
"Tags": "Étiquettes",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "anciens articles, page %d",
"page %d": "page %d",
}
diff --git a/nikola/data/themes/base/messages/messages_gl.py b/nikola/data/themes/base/messages/messages_gl.py
index c5c82ee..fd26d77 100644
--- a/nikola/data/themes/base/messages/messages_gl.py
+++ b/nikola/data/themes/base/messages/messages_gl.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "",
"Archive": "",
"Categories": "",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "",
"Posts about %s": "",
"Posts for year %s": "",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "",
"Previous post": "",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "",
"Skip to main content": "",
"Source": "",
+ "Subcategories:": "",
"Tags and Categories": "",
"Tags": "",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "",
"page %d": "",
}
diff --git a/nikola/data/themes/base/messages/messages_hi.py b/nikola/data/themes/base/messages/messages_hi.py
index 3d69697..7804b60 100644
--- a/nikola/data/themes/base/messages/messages_hi.py
+++ b/nikola/data/themes/base/messages/messages_hi.py
@@ -2,33 +2,38 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "पढ़ने में %d मिनट बाकी",
+ "(active)": "",
"Also available in:": "उपलब्ध भाषाएँ:",
"Archive": "आर्काइव",
"Categories": "श्रेणियाँ",
- "Comments": "",
+ "Comments": "टिप्पणियाँ",
"LANGUAGE": "हिन्दी",
- "Languages:": "",
+ "Languages:": "भाषाएँ:",
"More posts about %s": "%s के बारे में अौर पोस्टें",
"Newer posts": "नई पोस्टें",
"Next post": "अगली पोस्ट",
- "No posts found.": "",
- "Nothing found.": "",
+ "No posts found.": "कोई पोस्ट नहीं मिल सकी",
+ "Nothing found.": "कुछ नहीं मिल सका",
"Older posts": "पुरानी पोस्टें",
"Original site": "असली साइट",
"Posted:": "पोस्टेड:",
"Posts about %s": "%s के बारे में पोस्टें",
"Posts for year %s": "साल %s की पोस्टें",
+ "Posts for {month} {day}, {year}": "{day} {month} {year} की पोस्टें",
"Posts for {month} {year}": "{month} {year} की पोस्टें",
"Previous post": "पिछली पोस्ट",
- "Publication date": "",
- "RSS feed": "",
+ "Publication date": "प्रकाशन की तारीख",
+ "RSS feed": "आर एस एस फ़ीड",
"Read in English": "हिन्दी में पढ़िए",
"Read more": "और पढ़िए",
- "Skip to main content": "",
+ "Skip to main content": "मुख्य सामग्री पर जाएँ",
"Source": "सोर्स",
+ "Subcategories:": "",
"Tags and Categories": "टैग्स और श्रेणियाँ",
"Tags": "टैग्स",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "पुरानी पोस्टें, पृष्‍ठ %d",
"page %d": "पृष्‍ठ %d",
}
diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py
index f5f0886..d26fc85 100644
--- a/nikola/data/themes/base/messages/messages_hr.py
+++ b/nikola/data/themes/base/messages/messages_hr.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Također dostupno i u:",
"Archive": "Arhiva",
"Categories": "Kategorije",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Objavljeno:",
"Posts about %s": "Postovi o %s",
"Posts for year %s": "Postovi za godinu %s",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Postovi za {month} {year}",
"Previous post": "Prethodni post",
"Publication date": "Nadnevak objave",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Čitaj dalje",
"Skip to main content": "",
"Source": "Izvor",
+ "Subcategories:": "",
"Tags and Categories": "Tagovi i kategorije",
"Tags": "Tagovi",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "stari postovi, stranice %d",
"page %d": "stranice %d",
}
diff --git a/nikola/data/themes/base/messages/messages_id.py b/nikola/data/themes/base/messages/messages_id.py
new file mode 100644
index 0000000..475299f
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_id.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "%d menit tersisa untuk membaca",
+ "(active)": "(aktif)",
+ "Also available in:": "Juga tersedia dalam:",
+ "Archive": "Arsip",
+ "Categories": "Kategori",
+ "Comments": "Komentar",
+ "LANGUAGE": "Inggris",
+ "Languages:": "Bahasa:",
+ "More posts about %s": "Lebih banyak tulisan tentang %s",
+ "Newer posts": "Tulisan lebih baru",
+ "Next post": "Tulisan berikutnya",
+ "No posts found.": "Tidak ada tulisan yang ditemukan.",
+ "Nothing found.": "Tidak ditemukan.",
+ "Older posts": "Tulisan lebih lama",
+ "Original site": "Situs orisinal",
+ "Posted:": "Ditulis oleh:",
+ "Posts about %s": "Tulisan tentang %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}",
+ "Previous post": "Tulisan sebelumnya",
+ "Publication date": "Tanggal publikasi",
+ "RSS feed": "Sindikasi RSS",
+ "Read in English": "Baca dalam bahasa Inggris",
+ "Read more": "Baca selengkapnya",
+ "Skip to main content": "Lanjutkan ke konten utama",
+ "Source": "Sumber",
+ "Subcategories:": "Sub kategori:",
+ "Tags and Categories": "Tag dan Kategori",
+ "Tags": "Tag",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "tulisan lama, halaman %d",
+ "page %d": "halaman %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py
index 62442d4..91f8374 100644
--- a/nikola/data/themes/base/messages/messages_it.py
+++ b/nikola/data/themes/base/messages/messages_it.py
@@ -2,15 +2,16 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "ancora %d minuti",
- "Also available in:": "Anche disponibile in:",
+ "%d min remaining to read": "ulteriori %d minuti di lettura",
+ "(active)": "(attivo)",
+ "Also available in:": "Disponibile anche in:",
"Archive": "Archivio",
"Categories": "Categorie",
"Comments": "Commenti",
- "LANGUAGE": "Italiano",
+ "LANGUAGE": "Inglese",
"Languages:": "Lingue:",
"More posts about %s": "Altri articoli collegati %s",
- "Newer posts": "Articoli recenti",
+ "Newer posts": "Articoli più recenti",
"Next post": "Articolo successivo",
"No posts found.": "Nessun articolo trovato.",
"Nothing found.": "Non trovato.",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Pubblicato:",
"Posts about %s": "Articoli su %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}",
"Previous post": "Articolo precedente",
"Publication date": "Data di pubblicazione",
- "RSS feed": "Flusso RSS",
- "Read in English": "Leggi in italiano",
+ "RSS feed": "Feed RSS",
+ "Read in English": "Leggi in inglese",
"Read more": "Continua la lettura",
- "Skip to main content": "",
+ "Skip to main content": "Vai al testo principale",
"Source": "Sorgente",
- "Tags and Categories": "Tags e Categorie",
- "Tags": "Tags",
- "old posts, page %d": "pagina dei vecchi articoli %d",
+ "Subcategories:": "Sottocategorie:",
+ "Tags and Categories": "Tag e Categorie",
+ "Tags": "Tag",
+ "Write your page here.": "Scrivi qui la tua pagina.",
+ "Write your post here.": "Scrivi qui il tuo post.",
+ "old posts, page %d": "vecchi articoli, pagina %d",
"page %d": "pagina %d",
}
diff --git a/nikola/data/themes/base/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py
index cba5ee9..972794d 100644
--- a/nikola/data/themes/base/messages/messages_ja.py
+++ b/nikola/data/themes/base/messages/messages_ja.py
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "読込むまで残り %d 分",
+ "(active)": "",
"Also available in:": "他の言語で読む:",
"Archive": "過去の記事",
"Categories": "カテゴリー",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "投稿日時:",
"Posts about %s": "%sについての記事",
"Posts for year %s": "%s年の記事",
+ "Posts for {month} {day}, {year}": "{year}年{month}月{day}日の記事",
"Posts for {month} {year}": "{year}年{month}月の記事",
"Previous post": "前の記事",
"Publication date": "投稿日",
"RSS feed": "RSS フィード",
"Read in English": "日本語で読む",
"Read more": "続きを読む",
- "Skip to main content": "",
+ "Skip to main content": "メインコンテンツをスキップ",
"Source": "ソース",
+ "Subcategories:": "",
"Tags and Categories": "タグとカテゴリー",
"Tags": "タグ",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "前の記事 %dページ目",
"page %d": "ページ %d",
}
diff --git a/nikola/data/themes/base/messages/messages_ko.py b/nikola/data/themes/base/messages/messages_ko.py
new file mode 100644
index 0000000..6904492
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_ko.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "읽기 %d분 남음.",
+ "(active)": "",
+ "Also available in:": "",
+ "Archive": "저장소",
+ "Categories": "분류",
+ "Comments": "댓글",
+ "LANGUAGE": "영어",
+ "Languages:": "언어:",
+ "More posts about %s": "%s에 대한 또다른 포스트",
+ "Newer posts": "최신 포스트",
+ "Next post": "다음 포스트",
+ "No posts found.": "검색된 포스트 없음.",
+ "Nothing found.": "검색 결과 없음.",
+ "Older posts": "옛날 포스트",
+ "Original site": "출처",
+ "Posted:": "",
+ "Posts about %s": "%s에 대한 포스트",
+ "Posts for year %s": "%s년도 포스트",
+ "Posts for {month} {day}, {year}": "",
+ "Posts for {month} {year}": "{year}년 {month}월에 쓴 포스트",
+ "Previous post": "이전 포스트",
+ "Publication date": "발간일",
+ "RSS feed": "RSS 목록",
+ "Read in English": "영어로 읽기",
+ "Read more": "더 읽기",
+ "Skip to main content": "주 콘텐츠로 바로가기",
+ "Source": "원문",
+ "Subcategories:": "",
+ "Tags and Categories": "태그와 분류",
+ "Tags": "태그",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "이전 포스트, 페이지 %d",
+ "page %d": "페이지 %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py
index f4d6062..2533247 100644
--- a/nikola/data/themes/base/messages/messages_nb.py
+++ b/nikola/data/themes/base/messages/messages_nb.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "Også tilgjengelig på:",
"Archive": "Arkiv",
"Categories": "Kategorier",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Publisert:",
"Posts about %s": "Innlegg om %s",
"Posts for year %s": "Innlegg fra %s",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Innlegg fra {month} {year}",
"Previous post": "Forrige innlegg",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Les mer",
"Skip to main content": "",
"Source": "Kilde",
+ "Subcategories:": "",
"Tags and Categories": "Merker og kategorier",
"Tags": "Merker",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "eldre innlegg, side %d",
"page %d": "side %d",
}
diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py
index 4aa9147..0661ce1 100644
--- a/nikola/data/themes/base/messages/messages_nl.py
+++ b/nikola/data/themes/base/messages/messages_nl.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d min resterende leestijd ",
+ "(active)": "(actief)",
"Also available in:": "Ook beschikbaar in:",
"Archive": "Archief",
"Categories": "Categorieën",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Geplaatst:",
"Posts about %s": "Berichten over %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}",
"Previous post": "Vorig bericht",
"Publication date": "Publicatiedatum",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Lees verder",
"Skip to main content": "Ga door naar de hoofdinhoud",
"Source": "Bron",
+ "Subcategories:": "Subcategorieën",
"Tags and Categories": "Tags en Categorieën",
"Tags": "Tags",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "oude berichten, pagina %d",
"page %d": "pagina %d",
}
diff --git a/nikola/data/themes/base/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py
index b1d4e82..89aae0f 100644
--- a/nikola/data/themes/base/messages/messages_pl.py
+++ b/nikola/data/themes/base/messages/messages_pl.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
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",
"Categories": "Kategorie",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Opublikowano:",
"Posts about %s": "Posty o %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}",
"Previous post": "Poprzedni post",
"Publication date": "Data publikacji",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Czytaj więcej",
"Skip to main content": "Przejdź do treści",
"Source": "Źródło",
+ "Subcategories:": "Podkategorie:",
"Tags and Categories": "Tagi i Kategorie",
"Tags": "Tagi",
+ "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",
}
diff --git a/nikola/data/themes/base/messages/messages_pt.py b/nikola/data/themes/base/messages/messages_pt.py
index c5c82ee..fd26d77 100644
--- a/nikola/data/themes/base/messages/messages_pt.py
+++ b/nikola/data/themes/base/messages/messages_pt.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "",
"Archive": "",
"Categories": "",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "",
"Posts about %s": "",
"Posts for year %s": "",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "",
"Previous post": "",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "",
"Skip to main content": "",
"Source": "",
+ "Subcategories:": "",
"Tags and Categories": "",
"Tags": "",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "",
"page %d": "",
}
diff --git a/nikola/data/themes/base/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py
index 0805f8e..58ad63f 100644
--- a/nikola/data/themes/base/messages/messages_pt_br.py
+++ b/nikola/data/themes/base/messages/messages_pt_br.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d mín restante para leitura",
+ "(active)": "",
"Also available in:": "Também disponível em:",
"Archive": "Arquivo",
"Categories": "Categorias",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Publicado:",
"Posts about %s": "Posts sobre %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}",
"Previous post": "Post anterior",
"Publication date": "Data de publicação",
"RSS feed": "Feed RSS",
"Read in English": "Ler em português",
"Read more": "Leia mais",
- "Skip to main content": "",
+ "Skip to main content": "Pular para o conteúdo principal",
"Source": "Código",
+ "Subcategories:": "",
"Tags and Categories": "Tags e Categorias",
"Tags": "Tags",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "Posts antigos, página %d",
"page %d": "página %d",
}
diff --git a/nikola/data/themes/base/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py
index 7205906..cfbe536 100644
--- a/nikola/data/themes/base/messages/messages_ru.py
+++ b/nikola/data/themes/base/messages/messages_ru.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d минут чтения осталось",
+ "(active)": "",
"Also available in:": "Также доступно на:",
"Archive": "Архив",
"Categories": "Категории",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Опубликовано:",
"Posts about %s": "Записи о %s",
"Posts for year %s": "Записи за %s год",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "Записи за {month} {year}",
"Previous post": "Предыдущая запись",
"Publication date": "Дата опубликования",
"RSS feed": "RSS лента",
"Read in English": "Прочесть по-русски",
"Read more": "Читать далее",
- "Skip to main content": "",
+ "Skip to main content": "Перейти к главному содержимому",
"Source": "Источник",
+ "Subcategories:": "",
"Tags and Categories": "Тэги и категории",
"Tags": "Тэги",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "%d страница со старыми записями",
"page %d": "%d страница",
}
diff --git a/nikola/data/themes/base/messages/messages_si_lk.py b/nikola/data/themes/base/messages/messages_si_lk.py
index c5c82ee..fd26d77 100644
--- a/nikola/data/themes/base/messages/messages_si_lk.py
+++ b/nikola/data/themes/base/messages/messages_si_lk.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "",
"Archive": "",
"Categories": "",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "",
"Posts about %s": "",
"Posts for year %s": "",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "",
"Previous post": "",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "",
"Skip to main content": "",
"Source": "",
+ "Subcategories:": "",
"Tags and Categories": "",
"Tags": "",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "",
"page %d": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sk.py b/nikola/data/themes/base/messages/messages_sk.py
index e3618f3..a793ba6 100644
--- a/nikola/data/themes/base/messages/messages_sk.py
+++ b/nikola/data/themes/base/messages/messages_sk.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "zostáva %d minút na čítanie",
+ "(active)": "",
"Also available in:": "Tiež dostupné v:",
"Archive": "Archív",
"Categories": "Kategórie",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Zverejnené:",
"Posts about %s": "Príspevky o %s",
"Posts for year %s": "Príspevky z roku %s",
+ "Posts for {month} {day}, {year}": "Príspevky z dňa {day}. {month} {year}",
"Posts for {month} {year}": "Príspevky za mesiac {month} z roku {year}",
"Previous post": "Predchádzajúci príspevok",
"Publication date": "Dátum zverejnenia",
"RSS feed": "RSS kanál",
"Read in English": "Čítať v slovenčine",
"Read more": "Čítať ďalej",
- "Skip to main content": "",
+ "Skip to main content": "Skočiť na hlavný obsah",
"Source": "Zdroj",
+ "Subcategories:": "",
"Tags and Categories": "Štítky a kategórie",
"Tags": "Štítky",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "staré príspevky, strana %d",
"page %d": "stránka %d",
}
diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py
index f9f1d13..992787b 100644
--- a/nikola/data/themes/base/messages/messages_sl.py
+++ b/nikola/data/themes/base/messages/messages_sl.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "še %d min za branje preostanka",
+ "(active)": "",
"Also available in:": "Na voljo tudi v:",
"Archive": "Arhiv",
"Categories": "Kategorije",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "Objavljeno:",
"Posts about %s": "Objave o %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}",
"Previous post": "Prejšnja objava",
"Publication date": "Datum objave",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "Več o tem",
"Skip to main content": "Preskoči na glavno vsebino",
"Source": "Izvor",
+ "Subcategories:": "",
"Tags and Categories": "Značke in kategorije",
"Tags": "Značke",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "stare objave, stran %d",
"page %d": "stran %d",
}
diff --git a/nikola/data/themes/base/messages/messages_sr.py b/nikola/data/themes/base/messages/messages_sr.py
new file mode 100644
index 0000000..6087027
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_sr.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "%d минута је преостало за читање",
+ "(active)": "",
+ "Also available in:": "Такође доступан у:",
+ "Archive": "Архива",
+ "Categories": "Категорије",
+ "Comments": "Коментари",
+ "LANGUAGE": "Српски",
+ "Languages:": "Језици:",
+ "More posts about %s": "Више постова о %s",
+ "Newer posts": "Новији постови",
+ "Next post": "Следећи пост",
+ "No posts found.": "Нема постова.",
+ "Nothing found.": "Није ништа пронађено.",
+ "Older posts": "Старији постови",
+ "Original site": "Оригинал сајт",
+ "Posted:": "Објављено:",
+ "Posts about %s": "Постови о %s",
+ "Posts for year %s": "Постови за годину %s",
+ "Posts for {month} {day}, {year}": "",
+ "Posts for {month} {year}": "Постови за {month} {year}",
+ "Previous post": "Претходни пост",
+ "Publication date": "Датум објаве",
+ "RSS feed": "RSS feed",
+ "Read in English": "Прочитај на српском",
+ "Read more": "Прочитај више",
+ "Skip to main content": "Прескочи на главни садржај",
+ "Source": "Извор",
+ "Subcategories:": "",
+ "Tags and Categories": "Тагови и категорије",
+ "Tags": "Тагови",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "стари постови, страна %d",
+ "page %d": "страна %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_sv.py b/nikola/data/themes/base/messages/messages_sv.py
new file mode 100644
index 0000000..deda21a
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_sv.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "%d minuter kvar att läsa",
+ "(active)": "",
+ "Also available in:": "Även tillgänglig på:",
+ "Archive": "Arkiv",
+ "Categories": "Kategorier",
+ "Comments": "Kommentarer",
+ "LANGUAGE": "Svenska",
+ "Languages:": "Språk:",
+ "More posts about %s": "Mer inlägg om %s",
+ "Newer posts": "Nya inlägg",
+ "Next post": "Nästa inlägg",
+ "No posts found.": "Inga inlägg hittade",
+ "Nothing found.": "Inget hittat",
+ "Older posts": "Äldre inlägg",
+ "Original site": "Orgnialsida",
+ "Posted:": "Publicerad",
+ "Posts about %s": "Inlägg om %s",
+ "Posts for year %s": "Inlägg för år %s",
+ "Posts for {month} {day}, {year}": "Inlägg för {month} {day}, {year}",
+ "Posts for {month} {year}": "Inlägg för {month} {year}",
+ "Previous post": "Föregående inlägg",
+ "Publication date": "Publiceringsdatum",
+ "RSS feed": "RSS flöde",
+ "Read in English": "Läs på Svenska",
+ "Read more": "Läs mer",
+ "Skip to main content": "hoppa till huvudinehåll",
+ "Source": "Källa",
+ "Subcategories:": "Underkategorier:",
+ "Tags and Categories": "Taggar och Kategorier",
+ "Tags": "Taggar",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "gamla inlägg, sida %d",
+ "page %d": "sida %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_tl.py b/nikola/data/themes/base/messages/messages_tl.py
new file mode 100644
index 0000000..f748edd
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_tl.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "",
+ "(active)": "",
+ "Also available in:": "",
+ "Archive": "",
+ "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 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",
+ "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 3ba8217..21eb534 100644
--- a/nikola/data/themes/base/messages/messages_tr.py
+++ b/nikola/data/themes/base/messages/messages_tr.py
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
MESSAGES = {
- "%d min remaining to read": "",
+ "%d min remaining to read": "%d dakikalık okuma",
+ "(active)": "",
"Also available in:": "Şu dilde de mevcut:",
"Archive": "Arşiv",
"Categories": "Kategoriler",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "Yayın tarihi:",
"Posts about %s": "%s ile ilgili yazılar",
"Posts for year %s": "%s yılındaki yazılar",
+ "Posts for {month} {day}, {year}": "{month} {day}, {year} 'den beri olan yazılar",
"Posts for {month} {year}": "{month} {year} göre yazılar",
"Previous post": "Önceki yazı",
"Publication date": "Yayınlanma tarihi",
"RSS feed": "RSS kaynağı",
"Read in English": "Türkçe olarak oku",
"Read more": "Devamını oku",
- "Skip to main content": "",
+ "Skip to main content": "Ana içeriğe geç",
"Source": "Kaynak",
+ "Subcategories:": "",
"Tags and Categories": "Etiketler ve Kategoriler",
"Tags": "Etiketler",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "eski yazılar, sayfa %d",
"page %d": "sayfa %d",
}
diff --git a/nikola/data/themes/base/messages/messages_uk.py b/nikola/data/themes/base/messages/messages_uk.py
new file mode 100644
index 0000000..16913da
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_uk.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "Залишилось читати %d хвилин",
+ "(active)": "",
+ "Also available in:": "Іншою мовою:",
+ "Archive": "Архів",
+ "Categories": "Категорії",
+ "Comments": "Коментарі",
+ "LANGUAGE": "Українська",
+ "Languages:": "Мови:",
+ "More posts about %s": "Більше статей про %s",
+ "Newer posts": "Нові статті",
+ "Next post": "Наступна стаття",
+ "No posts found.": "Не знайдено жодної статті",
+ "Nothing found.": "Нічого не знайдено",
+ "Older posts": "Більш старі статті",
+ "Original site": "Оригінал сайту",
+ "Posted:": "Опублікована:",
+ "Posts about %s": "Статті про %s",
+ "Posts for year %s": "Статті за %s рік",
+ "Posts for {month} {day}, {year}": "Статті за {month} {day}, {year}",
+ "Posts for {month} {year}": "Статті за {month} {year}",
+ "Previous post": "Попередня стаття",
+ "Publication date": "Дата публікації",
+ "RSS feed": "RSS-стрічка",
+ "Read in English": "Читати українською",
+ "Read more": "Читати далі",
+ "Skip to main content": "Перейти до основного матеріалу",
+ "Source": "Джерело",
+ "Subcategories:": "",
+ "Tags and Categories": "Теги і категорії",
+ "Tags": "Теги",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "старі статті, сторінка %d",
+ "page %d": "сторінка %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py
index fac2a3e..075606c 100644
--- a/nikola/data/themes/base/messages/messages_ur.py
+++ b/nikola/data/themes/base/messages/messages_ur.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "%d منٹ کا مطالعہ باقی",
+ "(active)": "",
"Also available in:": "ان زبانوں میں بھی دستیاب:",
"Archive": "آرکائیو",
"Categories": "زمرے",
@@ -19,16 +20,20 @@ MESSAGES = {
"Posted:": "اشاعت:",
"Posts about %s": "%s کے بارے میں تحاریر",
"Posts for year %s": "سال %s کی تحاریر",
+ "Posts for {month} {day}, {year}": "{day} {month}، {year} کی تحاریر",
"Posts for {month} {year}": "{month} {year} کی تحاریر",
"Previous post": "پچھلی تحریر",
"Publication date": "تاریخِ اشاعت",
"RSS feed": "آر ایس ایس فیڈ",
- "Read in English": "اردو میں پڑھیے",
- "Read more": "مزید پڑھیے",
- "Skip to main content": "",
+ "Read in English": "اردو میں پڑھیں",
+ "Read more": "مزید پڑھیں",
+ "Skip to main content": "مرکزی متن پر جائیں",
"Source": "سورس",
+ "Subcategories:": "ذیلی زمرے",
"Tags and Categories": "ٹیگز اور زمرے",
"Tags": "ٹیگز",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "پرانی تحاریر صفحہ %d",
"page %d": "صفحہ %d",
}
diff --git a/nikola/data/themes/base/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py
index 9d36505..c21b4f0 100644
--- a/nikola/data/themes/base/messages/messages_zh_cn.py
+++ b/nikola/data/themes/base/messages/messages_zh_cn.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
MESSAGES = {
"%d min remaining to read": "",
+ "(active)": "",
"Also available in:": "其他语言版本:",
"Archive": "文章存档",
"Categories": "分类",
@@ -19,6 +20,7 @@ MESSAGES = {
"Posted:": "发表于:",
"Posts about %s": "文章分类:%s",
"Posts for year %s": "%s年文章",
+ "Posts for {month} {day}, {year}": "",
"Posts for {month} {year}": "{year}年{month}月文章",
"Previous post": "前一篇",
"Publication date": "",
@@ -27,8 +29,11 @@ MESSAGES = {
"Read more": "更多",
"Skip to main content": "",
"Source": "源代码",
+ "Subcategories:": "",
"Tags and Categories": "标签和分类",
"Tags": "标签",
+ "Write your page here.": "",
+ "Write your post here.": "",
"old posts, page %d": "旧文章页 %d",
"page %d": "",
}
diff --git a/nikola/data/themes/base/messages/messages_zh_tw.py b/nikola/data/themes/base/messages/messages_zh_tw.py
new file mode 100644
index 0000000..fd26d77
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_zh_tw.py
@@ -0,0 +1,39 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "%d min remaining to read": "",
+ "(active)": "",
+ "Also available in:": "",
+ "Archive": "",
+ "Categories": "",
+ "Comments": "",
+ "LANGUAGE": "",
+ "Languages:": "",
+ "More posts about %s": "",
+ "Newer posts": "",
+ "Next post": "",
+ "No posts found.": "",
+ "Nothing found.": "",
+ "Older posts": "",
+ "Original site": "",
+ "Posted:": "",
+ "Posts about %s": "",
+ "Posts for year %s": "",
+ "Posts for {month} {day}, {year}": "",
+ "Posts for {month} {year}": "",
+ "Previous post": "",
+ "Publication date": "",
+ "RSS feed": "",
+ "Read in English": "",
+ "Read more": "",
+ "Skip to main content": "",
+ "Source": "",
+ "Subcategories:": "",
+ "Tags and Categories": "",
+ "Tags": "",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "",
+ "page %d": "",
+}
diff --git a/nikola/data/themes/base/templates/archiveindex.tmpl b/nikola/data/themes/base/templates/archiveindex.tmpl
new file mode 100644
index 0000000..129b7c4
--- /dev/null
+++ b/nikola/data/themes/base/templates/archiveindex.tmpl
@@ -0,0 +1,13 @@
+## -*- coding: utf-8 -*-
+<%inherit file="index.tmpl"/>
+
+<%block name="extra_head">
+ ${parent.extra_head()}
+ %if len(translations) > 1 and generate_atom:
+ %for language in 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
+</%block>
diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl
index 21f5ad5..6da6416 100644
--- a/nikola/data/themes/base/templates/base.tmpl
+++ b/nikola/data/themes/base/templates/base.tmpl
@@ -14,7 +14,7 @@ ${template_hooks['extra_head']()}
<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
<div id="container">
${header.html_header()}
- <main id="content">
+ <main id="content" role="main">
<%block name="content"></%block>
</main>
${footer.html_footer()}
diff --git a/nikola/data/themes/base/templates/base_header.tmpl b/nikola/data/themes/base/templates/base_header.tmpl
index 0c6e12d..0c19af6 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('/')}" title="${blog_title}" rel="home">
+ <h1 id="brand"><a href="${abs_link(_link("root", None, lang))}" title="${blog_title}" rel="home">
%if logo_url:
<img src="${logo_url}" alt="${blog_title}" id="logo">
%endif
@@ -36,7 +36,7 @@
<ul>
%for suburl, text in url:
% if rel_link(permalink, suburl) == "#":
- <li class="active"><a href="${permalink}">${text}</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,7 +44,7 @@
</ul>
% else:
% if rel_link(permalink, url) == "#":
- <li class="active"><a href="${permalink}">${text}</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
diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl
index 491b6da..28b3f8a 100644
--- a/nikola/data/themes/base/templates/base_helper.tmpl
+++ b/nikola/data/themes/base/templates/base_helper.tmpl
@@ -2,28 +2,24 @@
<%def name="html_headstart()">
<!DOCTYPE html>
-<html
-\
-% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook'):
+<html \
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# \
+og: http://ogp.me/ns# article: http://ogp.me/ns/article# \
%endif
%if comment_system == 'facebook':
-fb: http://ogp.me/ns/fb# \
+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
-\
% if is_rtl:
dir="rtl" \
% endif
\
lang="${lang}">
- <head>
+<head>
<meta charset="utf-8">
%if description:
<meta name="description" content="${description}">
@@ -83,6 +79,10 @@ lang="${lang}">
<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_feedlinks()">
@@ -97,13 +97,22 @@ lang="${lang}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
%endif
%endif
+ %if generate_atom:
+ %if len(translations) > 1:
+ %for language in translations:
+ <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
+ %endfor
+ %else:
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
+ %endif
+ %endif
</%def>
<%def name="html_translations()">
<ul class="translations">
%for langname in translations.keys():
%if langname != lang:
- <li><a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
+ <li><a href="${abs_link(_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_disqus.tmpl b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
index 8a94eaf..6dd423c 100644
--- a/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
+++ b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
@@ -1,10 +1,6 @@
## -*- coding: utf-8 -*-
-<%!
- import json
- translations = {
- 'es': 'es_ES',
- }
-%>
+
+<%! import json %>
<%def name="comment_form(url, title, identifier)">
%if comment_system_id:
@@ -17,7 +13,11 @@
disqus_title=${json.dumps(title)},
disqus_identifier="${identifier}",
disqus_config = function () {
- this.language = "${translations.get(lang, lang)}";
+ %if lang == 'es':
+ this.language = "es_ES";
+ %else:
+ this.language = "${lang}";
+ %endif
};
(function() {
var dsq = document.createElement('script'); dsq.async = true;
diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/crumbs.tmpl
index de8e570..49c5e1e 100644
--- a/nikola/data/themes/base/templates/crumbs.tmpl
+++ b/nikola/data/themes/base/templates/crumbs.tmpl
@@ -5,7 +5,13 @@
<nav class="breadcrumbs">
<ul class="breadcrumb">
% for link, text in crumbs:
- <li><a href="${link}">${text}</a></li>
+ % if text != index_file:
+ % if link == '#':
+ <li>${text.rsplit('.html', 1)[0]}</li>
+ % else:
+ <li><a href="${link}">${text}</a></li>
+ % endif
+ % endif
% endfor
</ul>
</nav>
diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl
index ca9da05..0ee07ac 100644
--- a/nikola/data/themes/base/templates/gallery.tmpl
+++ b/nikola/data/themes/base/templates/gallery.tmpl
@@ -7,7 +7,7 @@
<%block name="content">
${ui.bar(crumbs)}
%if title:
- <h1>${title}</h1>
+ <h1>${title|h}</h1>
%endif
%if post:
<p>
@@ -34,3 +34,8 @@
${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">
+</%block>
diff --git a/nikola/data/themes/base/templates/index.tmpl b/nikola/data/themes/base/templates/index.tmpl
index e833eb0..69630e5 100644
--- a/nikola/data/themes/base/templates/index.tmpl
+++ b/nikola/data/themes/base/templates/index.tmpl
@@ -3,15 +3,23 @@
<%namespace name="comments" file="comments_helper.tmpl"/>
<%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
+</%block>
+
<%block name="content">
+<%block name="content_header"></%block>
<div class="postindex">
% for post in posts:
<article class="h-entry post-${post.meta('type')}">
<header>
- <h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()}</h1></a>
+ <h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()|h}</a></h1>
<div class="metadata">
<p class="byline author vcard"><span class="byline-name fn">${post.author()}</span></p>
- <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></a></p>
+ <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></a></p>
% if not post.meta('nocomments') and site_has_comments:
<p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}
% endif
diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl
index 0ef164f..4fb497c 100644
--- a/nikola/data/themes/base/templates/list_post.tmpl
+++ b/nikola/data/themes/base/templates/list_post.tmpl
@@ -9,7 +9,7 @@
%if posts:
<ul class="postlist">
% for post in posts:
- <li><a href="${post.permalink()}" class="listtitle">${post.title()}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></li>
+ <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li>
% endfor
</ul>
%else:
diff --git a/nikola/data/themes/base/templates/post.tmpl b/nikola/data/themes/base/templates/post.tmpl
index fc0afba..cbb81ef 100644
--- a/nikola/data/themes/base/templates/post.tmpl
+++ b/nikola/data/themes/base/templates/post.tmpl
@@ -10,15 +10,18 @@
<meta name="keywords" content="${post.meta('keywords')|h}">
% endif
%if post.description():
- <meta name="description" itemprop="description" content="${post.description()}">
+ <meta name="description" content="${post.description()}">
%endif
<meta name="author" content="${post.author()}">
%if post.prev_post:
- <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()}" type="text/html">
+ <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html">
%endif
%if post.next_post:
- <link rel="next" href="${post.next_post.permalink()}" title="${post.next_post.title()}" type="text/html">
+ <link rel="next" href="${post.next_post.permalink()}" title="${post.next_post.title()|h}" type="text/html">
%endif
+ % if post.is_draft:
+ <meta name="robots" content="noindex">
+ % endif
${helper.open_graph_metadata(post)}
${helper.twitter_card_information(post)}
${helper.meta_translations(post)}
@@ -37,7 +40,7 @@
</nav>
</aside>
% if not post.meta('nocomments') and site_has_comments:
- <section class="comments">
+ <section class="comments hidden-print">
<h2>${messages("Comments")}</h2>
${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)}
</section>
diff --git a/nikola/data/themes/base/templates/post_header.tmpl b/nikola/data/themes/base/templates/post_header.tmpl
index c848186..0efb2f8 100644
--- a/nikola/data/themes/base/templates/post_header.tmpl
+++ b/nikola/data/themes/base/templates/post_header.tmpl
@@ -4,12 +4,12 @@
<%def name="html_title()">
%if title and not post.meta('hidetitle'):
- <h1 class="p-name entry-title" itemprop="headline name"><a href="${post.permalink()}" class="u-url">${title|h}</a></h1>
+ <h1 class="p-name entry-title" itemprop="headline name"><a href="${post.permalink()}" class="u-url">${post.title()|h}</a></h1>
%endif
</%def>
<%def name="html_translations(post)">
- % if len(translations) > 1:
+ % if len(post.translated_to) > 1:
<div class="metadata posttranslations translations">
<h3 class="posttranslations-intro">${messages("Also available in:")}</h3>
% for langname in translations.keys():
@@ -32,13 +32,13 @@
${html_title()}
<div class="metadata">
<p class="byline author vcard"><span class="byline-name fn">${post.author()}</span></p>
- <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></a></p>
+ <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></a></p>
% if not post.meta('nocomments') and site_has_comments:
<p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}
% endif
${html_sourcelink()}
% if post.meta('link'):
- <p><a href='${post.meta('link')}'>${messages("Original site")}</a></p>
+ <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()}">
diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl
index c4e0ed1..e091447 100644
--- a/nikola/data/themes/base/templates/post_helper.tmpl
+++ b/nikola/data/themes/base/templates/post_helper.tmpl
@@ -14,7 +14,9 @@
%if post.tags:
<ul itemprop="keywords" class="tags">
%for tag in post.tags:
- <li><a class="tag p-category" href="${_link('tag', tag)}" rel="tag">${tag}</a></li>
+ % if tag not in hidden_tags:
+ <li><a class="tag p-category" href="${_link('tag', tag)}" rel="tag">${tag}</a></li>
+ % endif
%endfor
</ul>
%endif
@@ -22,15 +24,15 @@
<%def name="html_pager(post)">
%if post.prev_post or post.next_post:
- <ul class="pager">
+ <ul class="pager hidden-print">
%if post.prev_post:
<li class="previous">
- <a href="${post.prev_post.permalink()}" rel="prev" title="${post.prev_post.title()}">${messages("Previous post")}</a>
+ <a href="${post.prev_post.permalink()}" rel="prev" title="${post.prev_post.title()|h}">${messages("Previous post")}</a>
</li>
%endif
%if post.next_post:
<li class="next">
- <a href="${post.next_post.permalink()}" rel="next" title="${post.next_post.title()}">${messages("Next post")}</a>
+ <a href="${post.next_post.permalink()}" rel="next" title="${post.next_post.title()|h}">${messages("Next post")}</a>
</li>
%endif
</ul>
@@ -39,15 +41,30 @@
<%def name="open_graph_metadata(post)">
%if use_open_graph:
- <meta name="og:title" content="${post.title()[:70]|h}">
- <meta name="og:url" content="${abs_link(permalink)}">
+ <meta property="og:site_name" content="${blog_title|striphtml}">
+ <meta property="og:title" content="${post.title()[:70]|h}">
+ <meta property="og:url" content="${abs_link(permalink)}">
%if post.description():
- <meta name="og:description" content="${post.description()[:200]|h}">
+ <meta property="og:description" content="${post.description()[:200]|h}">
%else:
- <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}">
+ <meta property="og:description" content="${post.text(strip_html=True)[:200]|h}">
+ %endif
+ %if post.previewimage:
+ <meta property="og:image" content="${url_replacer(permalink, post.previewimage, lang, 'absolute')}">
+ %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()}">
+### %endif
+ %if post.date.isoformat():
+ <meta property="article:published_time" content="${post.date.isoformat()}">
+ %endif
+ %if post.tags:
+ %for tag in post.tags:
+ <meta property="article:tag" content="${tag}">
+ %endfor
%endif
- <meta name="og:site_name" content="${blog_title|striphtml}">
- <meta name="og:type" content="article">
%endif
</%def>
diff --git a/nikola/data/themes/base/templates/post_list_directive.tmpl b/nikola/data/themes/base/templates/post_list_directive.tmpl
index d9166e9..e080a81 100644
--- a/nikola/data/themes/base/templates/post_list_directive.tmpl
+++ b/nikola/data/themes/base/templates/post_list_directive.tmpl
@@ -8,7 +8,7 @@
<li class="post-list-item">
${post.formatted_date(date_format)}
&nbsp;
- <a href="${post.permalink(lang)}">${post.title(lang)}</a>
+ <a href="${post.permalink(lang)}">${post.title(lang)|h}</a>
</li>
% endfor
</ul>
diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl
index bff82c2..ec5caca 100644
--- a/nikola/data/themes/base/templates/tag.tmpl
+++ b/nikola/data/themes/base/templates/tag.tmpl
@@ -16,7 +16,18 @@
<%block name="content">
<article class="tagpage">
<header>
- <h1>${title}</h1>
+ <h1>${title|h}</h1>
+ %if description:
+ <p>${description}</p>
+ %endif
+ %if subcategories:
+ ${messages('Subcategories:')}
+ <ul>
+ %for name, link in subcategories:
+ <li><a href="${link}">${name}</a></li>
+ %endfor
+ </ul>
+ %endif
<div class="metadata">
%if len(translations) > 1 and generate_rss:
%for language in translations:
@@ -32,7 +43,7 @@
%if posts:
<ul class="postlist">
% for post in posts:
- <li><a href="${post.permalink()}" class="listtitle">${post.title()}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${messages("Publication date")}">${post.formatted_date(date_format)}</time></li>
+ <li><a href="${post.permalink()}" class="listtitle">${post.title()|h}</a> <time class="listdate" datetime="${post.date.isoformat()}" title="${post.formatted_date(date_format)}">${post.formatted_date(date_format)}</time></li>
% endfor
</ul>
%endif
diff --git a/nikola/data/themes/base/templates/tagindex.tmpl b/nikola/data/themes/base/templates/tagindex.tmpl
index 9dda70a..7160fe9 100644
--- a/nikola/data/themes/base/templates/tagindex.tmpl
+++ b/nikola/data/themes/base/templates/tagindex.tmpl
@@ -1,2 +1,24 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>
+
+<%block name="content_header">
+ %if subcategories:
+ ${messages('Subcategories:')}
+ <ul>
+ %for name, link in subcategories:
+ <li><a href="${link}">${name}</a></li>
+ %endfor
+ </ul>
+ %endif
+</%block>
+
+<%block name="extra_head">
+ ${parent.extra_head()}
+ %if len(translations) > 1 and generate_atom:
+ %for language in translations:
+ <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag} section (${language})" href="${_link(kind + "_atom", tag, language)}">
+ %endfor
+ %elif generate_atom:
+ <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag} section" href="${_link("tag" + "_atom", tag)}">
+ %endif
+</%block>
diff --git a/nikola/data/themes/base/templates/tags.tmpl b/nikola/data/themes/base/templates/tags.tmpl
index 3e0c4b4..a9d2dd7 100644
--- a/nikola/data/themes/base/templates/tags.tmpl
+++ b/nikola/data/themes/base/templates/tags.tmpl
@@ -7,14 +7,24 @@
<h1>${title}</h1>
</header>
% if cat_items:
- <h2>${messages("Categories")}</h2>
- <ul class="postlist">
- % for text, link in cat_items:
- % if text:
- <li><a class="reference" href="${link}">${text}</a></li>
+ % if items:
+ <h2>${messages("Categories")}</h2>
+ % endif
+ % for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
+ % for i in range(indent_change_before):
+ <ul class="postlist">
+ % endfor
+ <li><a class="reference" href="${link}">${text}</a>
+ % if indent_change_after <= 0:
+ </li>
% endif
+ % for i in range(-indent_change_after):
+ </ul>
+ % if i + 1 < len(indent_levels):
+ </li>
+ % endif
+ % endfor
% endfor
- </ul>
% if items:
<h2>${messages("Tags")}</h2>
% endif
@@ -22,7 +32,9 @@
% if items:
<ul class="postlist">
% for text, link in items:
- <li><a class="reference listtitle" href="${link}">${text}</a></li>
+ % if text not in hidden_tags:
+ <li><a class="reference listtitle" href="${link}">${text}</a></li>
+ % endif
% endfor
</ul>
% endif
diff --git a/nikola/data/themes/bootstrap-jinja/README.md b/nikola/data/themes/bootstrap-jinja/README.md
index 5340fe2..637caf5 100644
--- a/nikola/data/themes/bootstrap-jinja/README.md
+++ b/nikola/data/themes/bootstrap-jinja/README.md
@@ -3,7 +3,7 @@ A "website-done-with-bootstrap" theme, so to speak.
Has a fixed navigation bar at top that displays the NAVIGATION_LINKS
setting and supports nested menus.
-This theme is used in Nikola's website: http://getnikola.com
+This theme is used in Nikola's website: https://getnikola.com
Important: To fit in the bootstrap navigation bar, the search form needs the
navbar-form and pull-left CSS classes applied. Here is an example with Nikola's
diff --git a/nikola/data/themes/bootstrap-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap-jinja/assets/css/theme.css
index 7566a80..4fc31a8 120000..100644
--- a/nikola/data/themes/bootstrap-jinja/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap-jinja/assets/css/theme.css
@@ -1 +1,205 @@
-../../../bootstrap/assets/css/theme.css \ No newline at end of file
+#container {
+ width: 960px;
+ margin: 50 auto;
+}
+
+#contentcolumn {
+ max-width: 760px;
+}
+#q {
+ width: 150px;
+}
+
+img {
+ max-width: 90%;
+}
+
+.postbox {
+ border-bottom: 2px solid darkgrey;
+ margin-bottom: 12px;
+}
+
+.footerbox {padding: 15px; text-align: center; margin-bottom: 15px;}
+
+td.label {
+ /* Issue #290 */
+ background-color: inherit;
+}
+
+.footnote-reference {
+ /* Issue 290 */
+ vertical-align: super;
+ font-size: xx-small;
+}
+
+.caption {
+ /* Issue 292 */
+ text-align: center;
+ padding-top: 1em;
+}
+
+div.figure > img,
+div.figure > a > img {
+ /* Issue 292 */
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
+ /* Issue 277 */
+ border: 1px solid #aaa;
+ border-radius: 5px;
+}
+
+blockquote p, blockquote {
+ font-size: 17.5px;
+ font-weight: 300;
+ line-height: 1.25;
+}
+
+ul.bricks > li {
+ display: inline;
+ background-color: lightblue;
+ padding: 8px;
+ border-radius: 5px;
+ line-height: 3;
+ white-space:nowrap;
+ margin: 3px;
+}
+
+ul.breadcrumb > li:before {
+ content: " / ";
+}
+
+pre, pre code {
+ white-space: pre;
+ word-wrap: normal;
+ overflow: auto;
+}
+
+article.post-micro {
+ font-family: Georgia, 'Times New Roman', Times, serif;
+ font-size: 1.5em;
+}
+
+.image-block {
+ display: inline-block;
+}
+
+.flowr_row {
+ width: 100%;
+}
+
+.tags {
+ padding-left: 0;
+ margin-left: -5px;
+ list-style: none;
+ text-align: center;
+
+}
+
+.tags > li {
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ background-color: #999;
+ border-radius: 10px;
+}
+
+.tags > li a {
+ color: #fff;
+}
+
+.metadata p:before,
+.postlist .listdate:before {
+ content: " — ";
+}
+
+.metadata p:first-of-type:before {
+ content: "";
+}
+
+.metadata p {
+ display: inline;
+}
+
+.posttranslations h3 {
+ display: inline;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+.posttranslations h3:last-child {
+ display: none;
+}
+
+.entry-content {
+ margin-top: 1em;
+}
+
+.navbar .brand {
+ padding: 0 20px;
+}
+
+.navbar .brand #blog-title {
+ padding: 10px 0;
+ display: inline-block;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+}
+
+/* hat tip bootstrap/html5 boilerplate */
+@media print {
+ *, *:before, *:after {
+ font-family: Garamond, Junicode, serif;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ article .entry-title a[href]:after,
+ article .metadata a[href]:after,
+ article .tags a[href]:after {
+ content: "";
+ }
+
+ article .metadata .sourceline {
+ display: none;
+ }
+
+ article .metadata .linkline a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ .navbar {
+ display: none;
+ }
+}
diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
new file mode 120000
index 0000000..9b995d8
--- /dev/null
+++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
@@ -0,0 +1 @@
+../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
new file mode 120000
index 0000000..e20bd38
--- /dev/null
+++ b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
@@ -0,0 +1 @@
+../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js b/nikola/data/themes/bootstrap-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js
deleted file mode 120000
index 76f289e..0000000
--- a/nikola/data/themes/bootstrap-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/bootstrap-jinja/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js
index c195756..c0d986b 120000..100644
--- a/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js
+++ b/nikola/data/themes/bootstrap-jinja/assets/js/flowr.plugin.js
@@ -1 +1,265 @@
-../../../bootstrap/assets/js/flowr.plugin.js \ No newline at end of file
+/**
+ * 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 );
+
+ var newLineWidth = lineWidth + minWidth + requiredPadding(1);
+
+ if (minWidth > settings.maxWidth) {
+ // very short+wide images like panoramas
+ // show them even if ugly, as wide as possible
+ minWidth = settings.maxWidth-1;
+ minHeight = settings.height * minHeight / 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 resposive 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 = $this.data( 'data' ) || [];
+ 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);
+ 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);
+ // Set some basic stuff
+ displayObject
+ .css( 'width', displayData.width )
+ .css( 'height', displayData.height )
+ .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/bootstrap-jinja/assets/js/jquery.js b/nikola/data/themes/bootstrap-jinja/assets/js/jquery.js
new file mode 120000
index 0000000..966173b
--- /dev/null
+++ b/nikola/data/themes/bootstrap-jinja/assets/js/jquery.js
@@ -0,0 +1 @@
+../../../../../../bower_components/jquery/dist/jquery.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap-jinja/templates/base.tmpl
index e9bed3c..3177276 100644
--- a/nikola/data/themes/bootstrap-jinja/templates/base.tmpl
+++ b/nikola/data/themes/bootstrap-jinja/templates/base.tmpl
@@ -13,7 +13,7 @@
<!-- Menubar -->
-<div class="navbar navbar-fixed-top" id="navbar">
+<div class="navbar navbar-static-top" id="navbar">
<div class="navbar-inner">
<div class="container">
@@ -24,7 +24,7 @@
<span class="icon-bar"></span>
</a>
- <a class="brand" href="{{ abs_link('/') }}">
+ <a class="brand" href="{{ abs_link(_link("root", None, lang)) }}">
{% if logo_url %}
<img src="{{ logo_url }}" alt="{{ blog_title }}" id="logo">
{% endif %}
@@ -58,7 +58,7 @@
</div>
</div>
<!-- End of Menubar -->
-<div class="container-fluid" id="content">
+<div class="container-fluid" id="content" role="main">
<!--Body content-->
<div class="row-fluid">
<div class="span2"></div>
@@ -74,7 +74,13 @@
{{ template_hooks['page_footer']() }}
</div>
{{ base.late_load_js() }}
- <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
+ <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() }}
diff --git a/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl
index e44b3a7..4a29e69 100644
--- a/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl
+++ b/nikola/data/themes/bootstrap-jinja/templates/base_helper.tmpl
@@ -1,5 +1,6 @@
{# -*- coding: utf-8 -*- #}
+{% import 'annotation_helper.tmpl' as notes with context %}
{% macro html_headstart() %}
<!DOCTYPE html>
<html
@@ -68,19 +69,21 @@ lang="{{ lang }}">
{% macro late_load_js() %}
{% if use_bundles %}
{% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script src="/assets/js/all.js"></script>
{% else %}
<script src="/assets/js/all-nocdn.js"></script>
{% endif %}
{% else %}
{% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
{% else %}
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/moment-with-locales.min.js"></script>
+ <script src="/assets/js/fancydates.js"></script>
{% endif %}
<script src="/assets/js/jquery.colorbox-min.js"></script>
{% endif %}
@@ -94,14 +97,14 @@ lang="{{ lang }}">
{% macro html_stylesheets() %}
{% if use_bundles %}
{% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
{% else %}
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
{% endif %}
{% else %}
{% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
{% else %}
<link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
@@ -114,6 +117,10 @@ lang="{{ lang }}">
<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') %}
@@ -129,7 +136,7 @@ lang="{{ lang }}">
<ul class="dropdown-menu">
{% for suburl, text in url %}
{% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
+ <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
{% else %}
<li><a href="{{ suburl }}">{{ text }}</a>
{% endif %}
@@ -137,7 +144,7 @@ lang="{{ lang }}">
</ul>
{% else %}
{% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
+ <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
{% else %}
<li><a href="{{ url }}">{{ text }}</a>
{% endif %}
@@ -157,12 +164,21 @@ lang="{{ lang }}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}">
{% endif %}
{% endif %}
+ {% if generate_atom %}
+ {% if translations|length > 1 %}
+ {% for language in translations %}
+ <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}">
+ {% endfor %}
+ {% else %}
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}">
+ {% endif %}
+ {% endif %}
{% endmacro %}
{% macro html_translations() %}
{% for langname in translations.keys() %}
{% if langname != lang %}
- <li><a href="{{ _link("index", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
+ <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
{% endif %}
{% endfor %}
{% endmacro %}
diff --git a/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl b/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl
deleted file mode 100644
index e426774..0000000
--- a/nikola/data/themes/bootstrap-jinja/templates/bootstrap_helper.tmpl
+++ /dev/null
@@ -1,78 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{# Override only the functions that differ from base_helper.tmpl #}
-
-{% block html_stylesheets %}
- {% if use_bundles %}
- {% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- {% else %}
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- {% else %}
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
- {% endif %}
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
- {% if has_custom_css %}
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% endif %}
- {% if annotations and post and not post.meta('noannotations') %}
- {{ notes.css() }}
- {% elif not annotations and post and post.meta('annotations') %}
- {{ notes.css() }}
- {% endif %}
-{% endblock %}
-
-
-{% block late_load_js %}
- {% if use_bundles %}
- {% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
- <script src="/assets/js/all.js"></script>
- {% else %}
- <script src="/assets/js/all-nocdn.js"></script>
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
- {% else %}
- <script src="/assets/js/jquery-1.11.0.min.js"></script>
- <script src="/assets/js/bootstrap.min.js"></script>
- {% endif %}
- <script src="/assets/js/jquery.colorbox-min.js"></script>
- {% endif %}
-{% endblock %}
-
-
-{% block html_navigation_links %}
- {% for url, text in navigation_links[lang] %}
- {% if url is mapping %}
- <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }}<b class="caret"></b></a>
- <ul class="dropdown-menu">
- {% for suburl, text in url %}
- {% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
- {% else %}
- <li><a href="{{ suburl }}">{{ text }}</a>
- {% endif %}
- {% endfor %}
- </ul>
- {% else %}
- {% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
- {% else %}
- <li><a href="{{ url }}">{{ text }}</a>
- {% endif %}
- {% endif %}
- {% endfor %}
-{% endblock %}
diff --git a/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl b/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl
index e3f9f05..07112da 100644
--- a/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl
+++ b/nikola/data/themes/bootstrap-jinja/templates/gallery.tmpl
@@ -7,7 +7,7 @@
{% block content %}
{{ ui.bar(crumbs) }}
{% if title %}
- <h1>{{ title }}</h1>
+ <h1>{{ title|e }}</h1>
{% endif %}
{% if post %}
<p>
@@ -40,6 +40,7 @@
{% 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;
diff --git a/nikola/data/themes/bootstrap-jinja/templates/post.tmpl b/nikola/data/themes/bootstrap-jinja/templates/post.tmpl
index 54646d0..df4109c 100644
--- a/nikola/data/themes/bootstrap-jinja/templates/post.tmpl
+++ b/nikola/data/themes/bootstrap-jinja/templates/post.tmpl
@@ -14,10 +14,13 @@
{% endif %}
<meta name="author" content="{{ post.author() }}">
{% if post.prev_post %}
- <link rel="prev" href="{{ post.prev_post.permalink() }}" title="{{ post.prev_post.title() }}" type="text/html">
+ <link rel="prev" href="{{ post.prev_post.permalink() }}" title="{{ post.prev_post.title()|e }}" type="text/html">
{% endif %}
{% if post.next_post %}
- <link rel="next" href="{{ post.next_post.permalink() }}" title="{{ post.next_post.title() }}" type="text/html">
+ <link rel="next" href="{{ post.next_post.permalink() }}" title="{{ post.next_post.title()|e }}" type="text/html">
+ {% endif %}
+ {% if post.is_draft %}
+ <meta name="robots" content="noindex">
{% endif %}
{{ helper.open_graph_metadata(post) }}
{{ helper.twitter_card_information(post) }}
@@ -37,7 +40,7 @@
</nav>
</aside>
{% if not post.meta('nocomments') and site_has_comments %}
- <section class="comments">
+ <section class="comments hidden-print">
<h2>{{ messages("Comments") }}</h2>
{{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }}
</section>
diff --git a/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl b/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl
deleted file mode 100644
index b565244..0000000
--- a/nikola/data/themes/bootstrap-jinja/templates/post_header.tmpl
+++ /dev/null
@@ -1,40 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% import 'post_helper.tmpl' as helper with context %}
-{% import 'comments_helper.tmpl' as comments with context %}
-
-{% macro html_title() %}
- <h1 class="p-name entry-title" itemprop="headline name"><a href="{{ post.permalink() }}" class="u-url">{{ title|e }}</a></h1>
-{% endmacro %}
-
-{% macro html_translations(post) %}
- {% if translations|length > 1 %}
- <div class="metadata posttranslations translations">
- <h3 class="posttranslations-intro">{{ messages("Also available in:") }}</h3>
- {% for langname in translations.keys() %}
- {% if langname != lang and post.is_translation_available(langname) %}
- <p><a href="{{ post.permalink(langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></p>
- {% endif %}
- {% endfor %}
- </div>
- {% endif %}
-{% endmacro %}
-
-{% macro html_post_header() %}
- <header>
- {{ html_title() }}
- <div class="metadata">
- <p class="byline author vcard"><span class="byline-name fn">{{ post.author() }}</span></p>
- <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.date.isoformat() }}" itemprop="datePublished" title="{{ messages("Publication date") }}">{{ post.formatted_date(date_format) }}</time></a></p>
- {% if not post.meta('nocomments') and site_has_comments %}
- <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
- {% endif %}
- {% if post.meta('link') %}
- <p><a href='{{ post.meta('link') }}'>{{ messages("Original site") }}</a></p>
- {% endif %}
- {% if post.description() %}
- <meta content="{{ post.description() }}" itemprop="description">
- {% endif %}
- </div>
- {{ html_translations(post) }}
- </header>
-{% endmacro %}
diff --git a/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl b/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl
index 080e621..c308f19 100644
--- a/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl
+++ b/nikola/data/themes/bootstrap-jinja/templates/tags.tmpl
@@ -4,14 +4,24 @@
{% block content %}
<h1>{{ title }}</h1>
{% if cat_items %}
- <h2>{{ messages("Categories") }}</h2>
- <ul class="unstyled">
- {% for text, link in cat_items %}
- {% if text %}
- <li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
+ {% if items %}
+ <h2>{{ messages("Categories") }}</h2>
+ {% endif %}
+ {% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
+ {% for i in range(indent_change_before) %}
+ <ul class="unstyled">
+ {% endfor %}
+ <li><a class="reference badge" href="{{ link }}">{{ text }}</a>
+ {% if indent_change_after <= 0 %}
+ </li>
{% endif %}
+ {% for i in range(-indent_change_after) %}
+ </ul>
+ {% if i + 1 < indent_levels|length %}
+ </li>
+ {% endif %}
+ {% endfor %}
{% endfor %}
- </ul>
{% if items %}
<h2>{{ messages("Tags") }}</h2>
{% endif %}
@@ -19,7 +29,9 @@
{% if items %}
<ul class="list-inline">
{% for text, link in items %}
- <li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
+ {% if text not in hidden_tags %}
+ <li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
+ {% endif %}
{% endfor %}
</ul>
{% endif %}
diff --git a/nikola/data/themes/bootstrap/README.md b/nikola/data/themes/bootstrap/README.md
index 5340fe2..637caf5 100644
--- a/nikola/data/themes/bootstrap/README.md
+++ b/nikola/data/themes/bootstrap/README.md
@@ -3,7 +3,7 @@ A "website-done-with-bootstrap" theme, so to speak.
Has a fixed navigation bar at top that displays the NAVIGATION_LINKS
setting and supports nested menus.
-This theme is used in Nikola's website: http://getnikola.com
+This theme is used in Nikola's website: https://getnikola.com
Important: To fit in the bootstrap navigation bar, the search form needs the
navbar-form and pull-left CSS classes applied. Here is an example with Nikola's
diff --git a/nikola/data/themes/bootstrap/assets/css/theme.css b/nikola/data/themes/bootstrap/assets/css/theme.css
index 761dbb6..4fc31a8 100644
--- a/nikola/data/themes/bootstrap/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap/assets/css/theme.css
@@ -1,11 +1,3 @@
-body {
- padding-top: 60px;
-}
-@media (max-width: 979px) {
- body {
- padding-top: 0px;
- }
-}
#container {
width: 960px;
margin: 50 auto;
@@ -91,17 +83,6 @@ article.post-micro {
font-size: 1.5em;
}
-/* fix anchors for headers */
-h1, h2, h3 {
- margin-top: -40px;
- padding-top: 60px;
-}
-
-h4, h5, h6 {
- margin-top: -50px;
- padding-top: 60px;
-}
-
.image-block {
display: inline-block;
}
@@ -193,3 +174,32 @@ h4, h5, h6 {
overflow: visible;
clip: auto;
}
+
+/* hat tip bootstrap/html5 boilerplate */
+@media print {
+ *, *:before, *:after {
+ font-family: Garamond, Junicode, serif;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ article .entry-title a[href]:after,
+ article .metadata a[href]:after,
+ article .tags a[href]:after {
+ content: "";
+ }
+
+ article .metadata .sourceline {
+ display: none;
+ }
+
+ article .metadata .linkline a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ .navbar {
+ display: none;
+ }
+}
diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js
new file mode 120000
index 0000000..9b995d8
--- /dev/null
+++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-bn.js
@@ -0,0 +1 @@
+../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
new file mode 120000
index 0000000..e20bd38
--- /dev/null
+++ b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
@@ -0,0 +1 @@
+../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js b/nikola/data/themes/bootstrap/assets/js/colorbox-i18n/jquery.colorbox-pt-br.js
deleted file mode 120000
index 76f289e..0000000
--- a/nikola/data/themes/bootstrap/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/bootstrap/assets/js/jquery.js b/nikola/data/themes/bootstrap/assets/js/jquery.js
new file mode 120000
index 0000000..966173b
--- /dev/null
+++ b/nikola/data/themes/bootstrap/assets/js/jquery.js
@@ -0,0 +1 @@
+../../../../../../bower_components/jquery/dist/jquery.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap/bundles b/nikola/data/themes/bootstrap/bundles
index 089b036..38257d2 100644
--- a/nikola/data/themes/bootstrap/bundles
+++ b/nikola/data/themes/bootstrap/bundles
@@ -1,4 +1,4 @@
assets/css/all-nocdn.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,theme.css,custom.css
assets/css/all.css=rst.css,code.css,colorbox.css,theme.css,custom.css
-assets/js/all-nocdn.js=jquery.min.js,bootstrap.min.js,jquery.colorbox-min.js
-assets/js/all.js=jquery.colorbox-min.js
+assets/js/all-nocdn.js=jquery.min.js,bootstrap.min.js,jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js
+assets/js/all.js=jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js
diff --git a/nikola/data/themes/bootstrap/templates/base.tmpl b/nikola/data/themes/bootstrap/templates/base.tmpl
index 9f2bb61..e848673 100644
--- a/nikola/data/themes/bootstrap/templates/base.tmpl
+++ b/nikola/data/themes/bootstrap/templates/base.tmpl
@@ -13,7 +13,7 @@ ${template_hooks['extra_head']()}
<!-- Menubar -->
-<div class="navbar navbar-fixed-top" id="navbar">
+<div class="navbar navbar-static-top" id="navbar">
<div class="navbar-inner">
<div class="container">
@@ -24,7 +24,7 @@ ${template_hooks['extra_head']()}
<span class="icon-bar"></span>
</a>
- <a class="brand" href="${abs_link('/')}">
+ <a class="brand" href="${abs_link(_link("root", None, lang))}">
%if logo_url:
<img src="${logo_url}" alt="${blog_title}" id="logo">
%endif
@@ -58,7 +58,7 @@ ${template_hooks['extra_head']()}
</div>
</div>
<!-- End of Menubar -->
-<div class="container-fluid" id="content">
+<div class="container-fluid" id="content" role="main">
<!--Body content-->
<div class="row-fluid">
<div class="span2"></div>
@@ -74,7 +74,13 @@ ${template_hooks['extra_head']()}
${template_hooks['page_footer']()}
</div>
${base.late_load_js()}
- <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
+ <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()}
diff --git a/nikola/data/themes/bootstrap/templates/base_helper.tmpl b/nikola/data/themes/bootstrap/templates/base_helper.tmpl
index 40cce39..4c62f8d 100644
--- a/nikola/data/themes/bootstrap/templates/base_helper.tmpl
+++ b/nikola/data/themes/bootstrap/templates/base_helper.tmpl
@@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
+<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
<%def name="html_headstart()">
<!DOCTYPE html>
<html
@@ -68,19 +69,21 @@ lang="${lang}">
<%def name="late_load_js()">
%if use_bundles:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script src="/assets/js/all.js"></script>
%else:
<script src="/assets/js/all-nocdn.js"></script>
%endif
%else:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
%else:
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/moment-with-locales.min.js"></script>
+ <script src="/assets/js/fancydates.js"></script>
%endif
<script src="/assets/js/jquery.colorbox-min.js"></script>
%endif
@@ -94,14 +97,14 @@ lang="${lang}">
<%def name="html_stylesheets()">
%if use_bundles:
%if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
%else:
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
%endif
%else:
%if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
%else:
<link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
@@ -114,6 +117,10 @@ lang="${lang}">
<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'):
@@ -129,7 +136,7 @@ lang="${lang}">
<ul class="dropdown-menu">
%for suburl, text in url:
% if rel_link(permalink, suburl) == "#":
- <li class="active"><a href="${permalink}">${text}</a>
+ <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
%else:
<li><a href="${suburl}">${text}</a>
%endif
@@ -137,7 +144,7 @@ lang="${lang}">
</ul>
% else:
% if rel_link(permalink, url) == "#":
- <li class="active"><a href="${permalink}">${text}</a>
+ <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
%else:
<li><a href="${url}">${text}</a>
%endif
@@ -157,12 +164,21 @@ lang="${lang}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
%endif
%endif
+ %if generate_atom:
+ %if len(translations) > 1:
+ %for language in translations:
+ <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
+ %endfor
+ %else:
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
+ %endif
+ %endif
</%def>
<%def name="html_translations()">
%for langname in translations.keys():
%if langname != lang:
- <li><a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
+ <li><a href="${abs_link(_link("root", None, langname))}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
%endif
%endfor
</%def>
diff --git a/nikola/data/themes/bootstrap/templates/gallery.tmpl b/nikola/data/themes/bootstrap/templates/gallery.tmpl
index 8ad4eb4..ab15587 100644
--- a/nikola/data/themes/bootstrap/templates/gallery.tmpl
+++ b/nikola/data/themes/bootstrap/templates/gallery.tmpl
@@ -7,7 +7,7 @@
<%block name="content">
${ui.bar(crumbs)}
%if title:
- <h1>${title}</h1>
+ <h1>${title|h}</h1>
%endif
%if post:
<p>
@@ -40,6 +40,7 @@ ${comments.comment_form(None, permalink, title)}
<%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;
diff --git a/nikola/data/themes/bootstrap/templates/post.tmpl b/nikola/data/themes/bootstrap/templates/post.tmpl
index e55fcd5..8d9f88a 100644
--- a/nikola/data/themes/bootstrap/templates/post.tmpl
+++ b/nikola/data/themes/bootstrap/templates/post.tmpl
@@ -14,11 +14,14 @@
%endif
<meta name="author" content="${post.author()}">
%if post.prev_post:
- <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()}" type="text/html">
+ <link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html">
%endif
%if post.next_post:
- <link rel="next" href="${post.next_post.permalink()}" title="${post.next_post.title()}" type="text/html">
+ <link rel="next" href="${post.next_post.permalink()}" title="${post.next_post.title()|h}" type="text/html">
%endif
+ % if post.is_draft:
+ <meta name="robots" content="noindex">
+ % endif
${helper.open_graph_metadata(post)}
${helper.twitter_card_information(post)}
${helper.meta_translations(post)}
@@ -37,7 +40,7 @@
</nav>
</aside>
% if not post.meta('nocomments') and site_has_comments:
- <section class="comments">
+ <section class="comments hidden-print">
<h2>${messages("Comments")}</h2>
${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)}
</section>
diff --git a/nikola/data/themes/bootstrap/templates/tags.tmpl b/nikola/data/themes/bootstrap/templates/tags.tmpl
index 9afeca7..ead3b0a 100644
--- a/nikola/data/themes/bootstrap/templates/tags.tmpl
+++ b/nikola/data/themes/bootstrap/templates/tags.tmpl
@@ -4,14 +4,24 @@
<%block name="content">
<h1>${title}</h1>
% if cat_items:
- <h2>${messages("Categories")}</h2>
- <ul class="unstyled">
- % for text, link in cat_items:
- % if text:
- <li><a class="reference badge" href="${link}">${text}</a></li>
+ % if items:
+ <h2>${messages("Categories")}</h2>
+ % endif
+ % for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
+ % for i in range(indent_change_before):
+ <ul class="unstyled">
+ % endfor
+ <li><a class="reference badge" href="${link}">${text}</a>
+ % if indent_change_after <= 0:
+ </li>
% endif
+ % for i in range(-indent_change_after):
+ </ul>
+ % if i + 1 < len(indent_levels):
+ </li>
+ % endif
+ % endfor
% endfor
- </ul>
% if items:
<h2>${messages("Tags")}</h2>
% endif
@@ -19,7 +29,9 @@
% if items:
<ul class="list-inline">
% for text, link in items:
- <li><a class="reference badge" href="${link}">${text}</a></li>
+ % if text not in hidden_tags:
+ <li><a class="reference badge" href="${link}">${text}</a></li>
+ % endif
% endfor
</ul>
% endif
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css b/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
index b9cce36..189ea89 120000..100644
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
+++ b/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
@@ -1 +1,160 @@
-../../../bootstrap3/assets/css/docs.css \ No newline at end of file
+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/ie6/borderBottomCenter.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png
index 2a6267e..0d4475e 120000..100644
--- 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 differ
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
index 6cd025a..2775eba 120000..100644
--- 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 differ
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
index 9596518..f7f5137 120000..100644
--- 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 differ
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
index b5403bf..a2d63d1 120000..100644
--- 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 differ
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
index 27c023f..fd7c3e8 120000..100644
--- 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 differ
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
index e272a45..2937a9c 120000..100644
--- 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 differ
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
index e8ceae5..f9d458b 120000..100644
--- 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 differ
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
index 9a84403..74b8583 120000..100644
--- 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 differ
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/rst.css b/nikola/data/themes/bootstrap3-jinja/assets/css/rst.css
deleted file mode 120000
index d78763e..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/rst.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../bootstrap3/assets/css/rst.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css
index a2774ff..9ce6472 120000..100644
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css
@@ -1 +1,213 @@
-../../../bootstrap3/assets/css/theme.css \ No newline at end of file
+#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;
+}
+
+.titlebox {
+ text-align: right;
+}
+
+#addthisbox {margin-bottom: 12px;}
+
+td.label {
+ /* Issue #290 */
+ background-color: inherit;
+}
+
+.footnote-reference {
+ /* Issue 290 */
+ vertical-align: super;
+ font-size: xx-small;
+}
+
+
+.caption {
+ /* Issue 292 */
+ text-align: center;
+ padding-top: 1em;
+}
+
+div.figure > img,
+div.figure > a > img {
+ /* Issue 292 */
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
+ /* Issue 277 */
+ border: 1px solid #aaa;
+ border-radius: 5px;
+}
+
+blockquote p, blockquote {
+ font-size: 17.5px;
+ font-weight: 300;
+ line-height: 1.25;
+}
+
+ul.bricks > li {
+ display: inline;
+ background-color: lightblue;
+ padding: 8px;
+ border-radius: 5px;
+ line-height: 3;
+ white-space:nowrap;
+ margin: 3px;
+}
+
+.at300b, .stMainServices, .stButton, .stButton_gradient {
+ box-sizing: content-box;
+}
+
+pre, pre code {
+ white-space: pre;
+ word-wrap: normal;
+ overflow: auto;
+}
+
+article.post-micro {
+ font-family: Georgia, 'Times New Roman', Times, serif;
+ font-size: 1.5em;
+}
+
+.image-block {
+ display: inline-block;
+}
+
+.flowr_row {
+ width: 100%;
+}
+
+.tags {
+ padding-left: 0;
+ margin-left: -5px;
+ list-style: none;
+ text-align: center;
+
+}
+
+.tags > li {
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ background-color: #999;
+ border-radius: 10px;
+}
+
+.tags > li a {
+ color: #fff;
+}
+
+.metadata p:before,
+.postlist .listdate:before {
+ content: " — ";
+}
+
+.metadata p:first-of-type:before {
+ content: "";
+}
+
+.metadata p {
+ display: inline;
+}
+
+.posttranslations h3 {
+ display: inline;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+.posttranslations h3:last-child {
+ display: none;
+}
+
+.entry-content {
+ margin-top: 1em;
+}
+
+.navbar-brand {
+ padding: 0 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;
+}
+
+/* Custom page footer */
+#footer {
+ padding-top: 19px;
+ color: #777;
+ border-top: 1px solid #e5e5e5;
+}
+
+/* hat tip bootstrap/html5 boilerplate */
+@media print {
+ *, *:before, *:after {
+ font-family: Garamond, Junicode, serif;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ article .entry-title a[href]:after,
+ article .metadata a[href]:after,
+ article .tags a[href]:after {
+ content: "";
+ }
+
+ article .metadata .sourceline {
+ display: none;
+ }
+
+ article .metadata .linkline a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ .navbar {
+ display: none;
+ }
+}
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2
new file mode 120000
index 0000000..8c1e4d3
--- /dev/null
+++ b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2
@@ -0,0 +1 @@
+../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl
index c1ac838..058640a 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl
+++ b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl
@@ -13,7 +13,7 @@
<!-- Menubar -->
-<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container"><!-- This keeps the margins nice -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
@@ -22,7 +22,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
- <a class="navbar-brand" href="{{ abs_link('/') }}">
+ <a class="navbar-brand" href="{{ abs_link(_link("root", None, lang)) }}">
{% if logo_url %}
<img src="{{ logo_url }}" alt="{{ blog_title }}" id="logo">
{% endif %}
@@ -58,7 +58,7 @@
<!-- End of Menubar -->
-<div class="container" id="content">
+<div class="container" id="content" role="main">
<div class="body-content">
<!--Body content-->
<div class="row">
@@ -67,7 +67,7 @@
</div>
<!--End of body content-->
- <footer>
+ <footer id="footer">
{{ content_footer }}
{{ template_hooks['page_footer']() }}
</footer>
@@ -75,7 +75,13 @@
</div>
{{ base.late_load_js() }}
- <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
+ <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() }}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl
index 38a73c4..a481632 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl
+++ b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl
@@ -1,5 +1,6 @@
{# -*- coding: utf-8 -*- #}
+{% import 'annotation_helper.tmpl' as notes with context %}
{% macro html_headstart() %}
<!DOCTYPE html>
<html
@@ -67,19 +68,21 @@ lang="{{ lang }}">
{% macro late_load_js() %}
{% if use_bundles %}
{% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="/assets/js/all.js"></script>
{% else %}
<script src="/assets/js/all-nocdn.js"></script>
{% endif %}
{% else %}
{% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
{% else %}
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/moment-with-locales.min.js"></script>
+ <script src="/assets/js/fancydates.js"></script>
{% endif %}
<script src="/assets/js/jquery.colorbox-min.js"></script>
{% endif %}
@@ -93,14 +96,14 @@ lang="{{ lang }}">
{% macro html_stylesheets() %}
{% if use_bundles %}
{% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
{% else %}
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
{% endif %}
{% else %}
{% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
{% else %}
<link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
{% endif %}
@@ -112,6 +115,10 @@ lang="{{ lang }}">
<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') %}
@@ -122,11 +129,11 @@ lang="{{ lang }}">
{% macro html_navigation_links() %}
{% for url, text in navigation_links[lang] %}
{% if isinstance(url, tuple) %}
- <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }}<b class="caret"></b></a>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ text }} <b class="caret"></b></a>
<ul class="dropdown-menu">
{% for suburl, text in url %}
{% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
+ <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
{% else %}
<li><a href="{{ suburl }}">{{ text }}</a>
{% endif %}
@@ -134,7 +141,7 @@ lang="{{ lang }}">
</ul>
{% else %}
{% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }}</a>
+ <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
{% else %}
<li><a href="{{ url }}">{{ text }}</a>
{% endif %}
@@ -154,12 +161,21 @@ lang="{{ lang }}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}">
{% endif %}
{% endif %}
+ {% if generate_atom %}
+ {% if translations|length > 1 %}
+ {% for language in translations %}
+ <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}">
+ {% endfor %}
+ {% else %}
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}">
+ {% endif %}
+ {% endif %}
{% endmacro %}
{% macro html_translations() %}
{% for langname in translations.keys() %}
{% if langname != lang %}
- <li><a href="{{ _link("index", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
+ <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
index 11382c3..00fda11 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl
+++ b/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl
@@ -7,7 +7,7 @@
{% block content %}
{{ ui.bar(crumbs) }}
{% if title %}
- <h1>{{ title }}</h1>
+ <h1>{{ title|e }}</h1>
{% endif %}
{% if post %}
<p>
@@ -17,8 +17,7 @@
{% if folders %}
<ul>
{% for folder, ftitle in folders %}
- <li><a href="{{ folder }}"><i class="glyphicon
- glyphicon-folder-open"></i>&nbsp;{{ ftitle }}</a></li>
+ <li><a href="{{ folder }}"><i class="glyphicon glyphicon-folder-open"></i>&nbsp;{{ ftitle }}</a></li>
{% endfor %}
</ul>
{% endif %}
@@ -41,6 +40,7 @@
{% 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;
diff --git a/nikola/data/themes/bootstrap3/assets/css/rst.css b/nikola/data/themes/bootstrap3/assets/css/rst.css
deleted file mode 100644
index 489ceaa..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/rst.css
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
-:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 7514 2012-09-14 14:27:12Z 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 ; }
-
-dl.docutils dd {
- margin-bottom: 0.5em }
-
-object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
- overflow: hidden;
-}
-
-/* Uncomment (and remove this text!) to get bold-faced definition list terms
-dl.docutils dt {
- font-weight: bold }
-*/
-
-div.abstract {
- margin: 2em 5em }
-
-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 {
- margin: 2em ;
- border: medium outset ;
- padding: 1em }
-
-div.admonition p.admonition-title, div.hint p.admonition-title,
-div.important p.admonition-title, div.note p.admonition-title,
-div.tip p.admonition-title {
- font-weight: bold ;
- font-family: sans-serif }
-
-div.attention p.admonition-title, div.caution p.admonition-title,
-div.danger p.admonition-title, div.error p.admonition-title,
-div.warning p.admonition-title, .code .error {
- color: red ;
- font-weight: bold ;
- font-family: sans-serif }
-
-/* 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: 0 0 0.5em 1em ;
- border: medium outset ;
- padding: 1em ;
- background-color: #ffffee ;
- width: 40% ;
- float: right ;
- clear: right }
-
-div.sidebar p.rubric {
- font-family: sans-serif ;
- font-size: medium }
-
-div.system-messages {
- margin: 5em }
-
-div.system-messages h1 {
- color: red }
-
-div.system-message {
- border: medium outset ;
- padding: 1em }
-
-div.system-message p.system-message-title {
- color: red ;
- font-weight: bold }
-
-div.topic {
- margin: 2em }
-
-h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
-h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
- margin-top: 0.4em }
-
-h1.title {
- text-align: center }
-
-h2.subtitle {
- text-align: center }
-
-hr.docutils {
- width: 75% }
-
-img.align-left, .figure.align-left, object.align-left {
- clear: left ;
- float: left ;
- 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 */
-div.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-title {
- font-family: sans-serif ;
- font-weight: bold ;
- font-size: larger }
-
-p.sidebar-subtitle {
- font-family: sans-serif ;
- font-weight: bold }
-
-p.topic-title {
- font-weight: bold }
-
-pre.address {
- margin-bottom: 0 ;
- margin-top: 0 ;
- font: inherit }
-
-pre.literal-block, pre.doctest-block, pre.math, pre.code {
- margin-left: 2em ;
- margin-right: 2em }
-
-pre.code .ln { color: grey; } /* line numbers */
-pre.code, code { background-color: #eeeeee }
-pre.code .comment, code .comment { color: #5C6576 }
-pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
-pre.code .literal.string, code .literal.string { color: #0C5404 }
-pre.code .name.builtin, code .name.builtin { color: #352B84 }
-pre.code .deleted, code .deleted { background-color: #DEB0A1}
-pre.code .inserted, code .inserted { background-color: #A3D289}
-
-span.classifier {
- font-family: sans-serif ;
- font-style: oblique }
-
-span.classifier-delimiter {
- font-family: sans-serif ;
- font-weight: bold }
-
-span.interpreted {
- font-family: sans-serif }
-
-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 }
-
-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 }
diff --git a/nikola/data/themes/bootstrap3/assets/css/theme.css b/nikola/data/themes/bootstrap3/assets/css/theme.css
index 5e3775a..9ce6472 100644
--- a/nikola/data/themes/bootstrap3/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap3/assets/css/theme.css
@@ -1,7 +1,3 @@
-body {
- margin-top: 60px;
-}
-
#container {
width: 960px;
margin: 0 auto;
@@ -92,17 +88,6 @@ article.post-micro {
font-size: 1.5em;
}
-/* fix anchors for headers */
-h1, h2, h3 {
- margin-top: -40px;
- padding-top: 60px;
-}
-
-h4, h5, h6 {
- margin-top: -50px;
- padding-top: 60px;
-}
-
.image-block {
display: inline-block;
}
@@ -178,6 +163,51 @@ h4, h5, h6 {
max-width: 100%;
}
+.navbar-brand>img {
+ display: inline;
+}
+
.row {
margin: 0;
}
+
+/* for alignment with Bootstrap's .entry-content styling */
+.entry-summary {
+ margin-top: 1em;
+}
+
+/* Custom page footer */
+#footer {
+ padding-top: 19px;
+ color: #777;
+ border-top: 1px solid #e5e5e5;
+}
+
+/* hat tip bootstrap/html5 boilerplate */
+@media print {
+ *, *:before, *:after {
+ font-family: Garamond, Junicode, serif;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ article .entry-title a[href]:after,
+ article .metadata a[href]:after,
+ article .tags a[href]:after {
+ content: "";
+ }
+
+ article .metadata .sourceline {
+ display: none;
+ }
+
+ article .metadata .linkline a[href]:after {
+ content: " (" attr(href) ")";
+ }
+
+ .navbar {
+ display: none;
+ }
+}
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2
new file mode 120000
index 0000000..8c1e4d3
--- /dev/null
+++ b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2
@@ -0,0 +1 @@
+../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/bundles b/nikola/data/themes/bootstrap3/bundles
index 0a96b4f..8bdc591 100644
--- a/nikola/data/themes/bootstrap3/bundles
+++ b/nikola/data/themes/bootstrap3/bundles
@@ -1,4 +1,4 @@
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
-assets/js/all.js=jquery.colorbox-min.js
+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/templates/base.tmpl b/nikola/data/themes/bootstrap3/templates/base.tmpl
index c463873..16aebce 100644
--- a/nikola/data/themes/bootstrap3/templates/base.tmpl
+++ b/nikola/data/themes/bootstrap3/templates/base.tmpl
@@ -13,7 +13,7 @@ ${template_hooks['extra_head']()}
<!-- Menubar -->
-<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container"><!-- This keeps the margins nice -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
@@ -22,7 +22,7 @@ ${template_hooks['extra_head']()}
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
- <a class="navbar-brand" href="${abs_link('/')}">
+ <a class="navbar-brand" href="${abs_link(_link("root", None, lang))}">
%if logo_url:
<img src="${logo_url}" alt="${blog_title}" id="logo">
%endif
@@ -58,7 +58,7 @@ ${template_hooks['extra_head']()}
<!-- End of Menubar -->
-<div class="container" id="content">
+<div class="container" id="content" role="main">
<div class="body-content">
<!--Body content-->
<div class="row">
@@ -67,7 +67,7 @@ ${template_hooks['extra_head']()}
</div>
<!--End of body content-->
- <footer>
+ <footer id="footer">
${content_footer}
${template_hooks['page_footer']()}
</footer>
@@ -75,7 +75,13 @@ ${template_hooks['extra_head']()}
</div>
${base.late_load_js()}
- <script>jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
+ <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()}
diff --git a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl
index 096c3c2..8ea843a 100644
--- a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl
+++ b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl
@@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
+<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
<%def name="html_headstart()">
<!DOCTYPE html>
<html
@@ -67,19 +68,21 @@ lang="${lang}">
<%def name="late_load_js()">
%if use_bundles:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="/assets/js/all.js"></script>
%else:
<script src="/assets/js/all-nocdn.js"></script>
%endif
%else:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
- <script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
%else:
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/moment-with-locales.min.js"></script>
+ <script src="/assets/js/fancydates.js"></script>
%endif
<script src="/assets/js/jquery.colorbox-min.js"></script>
%endif
@@ -93,14 +96,14 @@ lang="${lang}">
<%def name="html_stylesheets()">
%if use_bundles:
%if use_cdn:
- <link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
%else:
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
%endif
%else:
%if use_cdn:
- <link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
+ <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
%else:
<link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
%endif
@@ -112,6 +115,10 @@ lang="${lang}">
<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'):
@@ -122,11 +129,11 @@ lang="${lang}">
<%def name="html_navigation_links()">
%for url, text in navigation_links[lang]:
% if isinstance(url, tuple):
- <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">${text}<b class="caret"></b></a>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">${text} <b class="caret"></b></a>
<ul class="dropdown-menu">
%for suburl, text in url:
% if rel_link(permalink, suburl) == "#":
- <li class="active"><a href="${permalink}">${text}</a>
+ <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
%else:
<li><a href="${suburl}">${text}</a>
%endif
@@ -134,7 +141,7 @@ lang="${lang}">
</ul>
% else:
% if rel_link(permalink, url) == "#":
- <li class="active"><a href="${permalink}">${text}</a>
+ <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
%else:
<li><a href="${url}">${text}</a>
%endif
@@ -154,12 +161,21 @@ lang="${lang}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
%endif
%endif
+ %if generate_atom:
+ %if len(translations) > 1:
+ %for language in translations:
+ <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
+ %endfor
+ %else:
+ <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
+ %endif
+ %endif
</%def>
<%def name="html_translations()">
%for langname in translations.keys():
%if langname != lang:
- <li><a href="${_link("index", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
+ <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
index 26fe80d..3d6c01a 100644
--- a/nikola/data/themes/bootstrap3/templates/gallery.tmpl
+++ b/nikola/data/themes/bootstrap3/templates/gallery.tmpl
@@ -7,7 +7,7 @@
<%block name="content">
${ui.bar(crumbs)}
%if title:
- <h1>${title}</h1>
+ <h1>${title|h}</h1>
%endif
%if post:
<p>
@@ -17,8 +17,7 @@
%if folders:
<ul>
% for folder, ftitle in folders:
- <li><a href="${folder}"><i class="glyphicon
- glyphicon-folder-open"></i>&nbsp;${ftitle}</a></li>
+ <li><a href="${folder}"><i class="glyphicon glyphicon-folder-open"></i>&nbsp;${ftitle}</a></li>
% endfor
</ul>
%endif
@@ -41,6 +40,7 @@ ${comments.comment_form(None, permalink, title)}
<%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;
diff --git a/nikola/filters.py b/nikola/filters.py
index 0037004..269aae9 100644
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -147,11 +147,67 @@ def jpegoptim(infile):
return runinplace(r"jpegoptim -p --strip-all -q %1", infile)
+def html_tidy_withconfig(infile):
+ return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent -config tidy5.conf -modify %1")
+
+
+def html_tidy_nowrap(infile):
+ return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1")
+
+
+def html_tidy_wrap(infile):
+ return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1")
+
+
+def html_tidy_wrap_attr(infile):
+ return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes yes --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1")
+
+
+def html_tidy_mini(infile):
+ return _html_tidy_runner(infile, r"-quiet --show-info no --show-warnings no -utf8 --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --tidy-mark no --drop-empty-elements no -modify %1")
+
+
+def _html_tidy_runner(infile, options):
+ """ Warnings (returncode 1) are not critical, and *everything* is a warning """
+ try:
+ status = runinplace(r"tidy5 " + options, infile)
+ except subprocess.CalledProcessError as err:
+ status = 0 if err.returncode == 1 else err.returncode
+ return status
+
+
+@apply_to_text_file
+def html5lib_minify(data):
+ import html5lib
+ import html5lib.serializer
+ data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'),
+ tree='lxml',
+ quote_attr_values=False,
+ omit_optional_tags=True,
+ minimize_boolean_attributes=True,
+ strip_whitespace=True,
+ alphabetical_attributes=True,
+ escape_lt_in_attrs=True)
+ return data
+
+
+@apply_to_text_file
+def html5lib_xmllike(data):
+ import html5lib
+ import html5lib.serializer
+ data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'),
+ tree='lxml',
+ quote_attr_values=True,
+ omit_optional_tags=False,
+ strip_whitespace=False,
+ alphabetical_attributes=True,
+ escape_lt_in_attrs=True)
+ return data
+
+
@apply_to_text_file
def minify_lines(data):
- datalines = data.splitlines()
- datalines = [line.lstrip() for line in datalines if not (line.strip() == "")]
- return "\n".join(datalines)
+ return data
@apply_to_text_file
@@ -169,6 +225,21 @@ def typogrify(data):
@apply_to_text_file
+def typogrify_sans_widont(data):
+ # typogrify with widont disabled because it caused broken headline
+ # wrapping, see issue #1465
+ if typo is None:
+ req_missing(['typogrify'], 'use the typogrify_sans_widont filter')
+
+ 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
+
+
+@apply_to_text_file
def php_template_injection(data):
import re
template = re.search('<\!-- __NIKOLA_PHP_TEMPLATE_INJECTION source\:(.*) checksum\:(.*)__ -->', data)
@@ -179,5 +250,6 @@ def php_template_injection(data):
_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)
-
- return phpdata
+ return phpdata
+ else:
+ return data
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
new file mode 100644
index 0000000..1e11a50
--- /dev/null
+++ b/nikola/image_processing.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2014 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+import datetime
+import os
+
+from nikola import utils
+
+Image = None
+try:
+ from PIL import Image, ExifTags # NOQA
+except ImportError:
+ try:
+ import Image as _Image
+ import ExifTags
+ Image = _Image
+ except ImportError:
+ pass
+
+
+class ImageProcessor(object):
+ """Apply image operations."""
+
+ image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+
+ def resize_image(self, src, dst, max_size, bigger_panoramas=True):
+ """Make a copy of the image in the requested size."""
+ if not Image:
+ utils.copy_file(src, dst)
+ return
+ im = Image.open(src)
+ w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+
+ # Panoramas get larger thumbnails because they look *awful*
+ if bigger_panoramas and w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+
+ if decoded == 'Orientation':
+ if value == 3:
+ im = im.rotate(180)
+ elif value == 6:
+ im = im.rotate(270)
+ elif value == 8:
+ im = im.rotate(90)
+ break
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(dst)
+ except Exception as e:
+ self.logger.warn("Can't thumbnail {0}, using original "
+ "image as thumbnail ({1})".format(src, e))
+ utils.copy_file(src, dst)
+ else: # Image is small
+ utils.copy_file(src, dst)
+
+ def image_date(self, src):
+ """Try to figure out the date of the image."""
+ if src not in self.dates:
+ try:
+ im = Image.open(src)
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+ if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):
+ try:
+ self.dates[src] = datetime.datetime.strptime(
+ value, r'%Y:%m:%d %H:%M:%S')
+ break
+ except ValueError: # Invalid EXIF date.
+ pass
+ if src not in self.dates:
+ self.dates[src] = datetime.datetime.fromtimestamp(
+ os.stat(src).st_mtime)
+ return self.dates[src]
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 6a3fc0d..2a15568 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,45 +30,32 @@ from collections import defaultdict
from copy import copy
from pkg_resources import resource_filename
import datetime
-import glob
import locale
import os
+import json
import sys
+import natsort
import mimetypes
try:
- from urlparse import urlparse, urlsplit, urljoin
+ from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote
except ImportError:
- from urllib.parse import urlparse, urlsplit, urljoin # NOQA
+ from urllib.parse import urlparse, urlsplit, urlunsplit, urljoin, unquote # NOQA
-from blinker import signal
try:
import pyphen
except ImportError:
pyphen = None
-import dateutil.tz
+import dateutil.tz
import logging
-from . import DEBUG
-
-if DEBUG:
- logging.basicConfig(level=logging.DEBUG)
-else:
- logging.basicConfig(level=logging.ERROR)
-
import PyRSS2Gen as rss
-
+import lxml.etree
import lxml.html
from yapsy.PluginManager import PluginManager
+from blinker import signal
-# Default "Read more..." link
-DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
-DEFAULT_RSS_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>'
-
-# Default pattern for translation files' names
-DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
-
-from .post import Post
-from . import utils
+from .post import Post # NOQA
+from . import DEBUG, utils
from .plugin_categories import (
Command,
LateTask,
@@ -79,8 +66,22 @@ from .plugin_categories import (
TaskMultiplier,
TemplateSystem,
SignalHandler,
+ ConfigPlugin,
+ PostScanner,
)
+if DEBUG:
+ logging.basicConfig(level=logging.DEBUG)
+else:
+ logging.basicConfig(level=logging.ERROR)
+
+# Default "Read more..." link
+DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
+DEFAULT_RSS_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>'
+
+# Default pattern for translation files' names
+DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
+
config_changed = utils.config_changed
@@ -98,9 +99,12 @@ LEGAL_VALUES = {
'muut',
],
'TRANSLATIONS': {
+ 'ar': 'Arabic',
+ 'az': 'Azerbaijani',
'bg': 'Bulgarian',
'ca': 'Catalan',
('cs', 'cz'): 'Czech',
+ 'da': 'Danish',
'de': 'German',
('el', '!gr'): 'Greek',
'en': 'English',
@@ -113,8 +117,10 @@ LEGAL_VALUES = {
'fr': 'French',
'hi': 'Hindi',
'hr': 'Croatian',
+ 'id': 'Indonesian',
'it': 'Italian',
('ja', '!jp'): 'Japanese',
+ 'ko': 'Korean',
'nb': 'Norwegian Bokmål',
'nl': 'Dutch',
'pl': 'Polish',
@@ -122,8 +128,11 @@ LEGAL_VALUES = {
'ru': 'Russian',
'sk': 'Slovak',
'sl': 'Slovene',
+ 'sr': 'Serbian (Cyrillic)',
+ 'sv': 'Swedish',
('tr', '!tr_TR'): 'Turkish',
'ur': 'Urdu',
+ 'uk': 'Ukrainian',
'zh_cn': 'Chinese (Simplified)',
},
'_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS': {
@@ -132,15 +141,17 @@ LEGAL_VALUES = {
# language with a different country, ``nikola init`` (but nobody else!)
# will accept it, warning the user about it.
'pt': 'pt_br',
- 'zh': 'zh_cn'
+ 'zh': 'zh_cn',
},
'RTL_LANGUAGES': ('ar', 'fa', 'ur'),
'COLORBOX_LOCALES': defaultdict(
str,
+ ar='ar',
bg='bg',
ca='ca',
cs='cs',
cz='cs',
+ da='da',
de='de',
en='',
es='es',
@@ -149,18 +160,80 @@ LEGAL_VALUES = {
fi='fi',
fr='fr',
hr='hr',
+ id='id',
it='it',
ja='ja',
+ ko='kr', # kr is South Korea, ko is the Korean language
nb='no',
nl='nl',
- pt_br='pt-br',
pl='pl',
+ 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
+ sv='sv',
tr='tr',
+ uk='uk',
zh_cn='zh-CN'
- )
+ ),
+ 'MOMENTJS_LOCALES': defaultdict(
+ str,
+ ar='ar',
+ bg='bg',
+ bn='bn',
+ ca='ca',
+ cs='cs',
+ cz='cs',
+ da='da',
+ de='de',
+ en='',
+ es='es',
+ et='et',
+ fa='fa',
+ fi='fi',
+ fr='fr',
+ hr='hr',
+ id='id',
+ it='it',
+ ja='ja',
+ ko='ko',
+ nb='nb',
+ nl='nl',
+ pl='pl',
+ pt_br='pt-br',
+ ru='ru',
+ sk='sk',
+ sl='sl',
+ sr='sr-cyrl',
+ sv='sv',
+ tr='tr',
+ zh_cn='zh-cn'
+ ),
+ 'PYPHEN_LOCALES': {
+ 'bg': 'bg',
+ 'ca': 'ca',
+ 'cs': 'cs',
+ 'cz': 'cs',
+ 'da': 'da',
+ 'de': 'de',
+ 'el': 'el',
+ 'en': 'en',
+ 'es': 'es',
+ 'et': 'et',
+ 'fr': 'fr',
+ 'hr': 'hr',
+ 'it': 'it',
+ 'nb': 'nb',
+ 'nl': 'nl',
+ 'pl': 'pl',
+ 'pt_br': 'pt_BR',
+ 'ru': 'ru',
+ 'sk': 'sk',
+ 'sl': 'sl',
+ 'sr': 'sr',
+ 'sv': 'sv',
+ },
}
@@ -188,12 +261,13 @@ class Nikola(object):
self.path_handlers = {
'slug': self.slug_path,
'post_path': self.post_path,
+ 'root': self.root_path,
'filename': self.filename_path,
}
self.strict = False
- self.global_data = {}
self.posts = []
+ self.all_posts = []
self.posts_per_year = defaultdict(list)
self.posts_per_month = defaultdict(list)
self.posts_per_tag = defaultdict(list)
@@ -209,8 +283,13 @@ class Nikola(object):
self.colorful = config.pop('__colorful__', False)
self.invariant = config.pop('__invariant__', False)
self.quiet = config.pop('__quiet__', False)
+ self._doit_config = config.pop('DOIT_CONFIG', {})
+ self.original_cwd = config.pop('__cwd__', False)
+ self.configuration_filename = config.pop('__configuration_filename__', False)
self.configured = bool(config)
+ self.injected_deps = defaultdict(list)
+ self.rst_transforms = []
self.template_hooks = {
'extra_head': utils.TemplateHookRegistry('extra_head', self),
'body_end': utils.TemplateHookRegistry('body_end', self),
@@ -228,11 +307,18 @@ class Nikola(object):
'ANNOTATIONS': False,
'ARCHIVE_PATH': "",
'ARCHIVE_FILENAME': "archive.html",
+ 'ARCHIVES_ARE_INDEXES': False,
'BLOG_AUTHOR': 'Default Author',
'BLOG_TITLE': 'Default Title',
'BLOG_DESCRIPTION': 'Default Description',
'BODY_END': "",
'CACHE_FOLDER': 'cache',
+ '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_PREFIX': 'cat_',
+ 'CATEGORY_ALLOW_HIERARCHIES': False,
+ 'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
'CODE_COLOR_SCHEME': 'default',
'COMMENT_SYSTEM': 'disqus',
'COMMENTS_IN_GALLERIES': False,
@@ -252,54 +338,68 @@ class Nikola(object):
'COPY_SOURCES': True,
'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_FANCINESS': 0,
'DEFAULT_LANG': "en",
- 'DEPLOY_COMMANDS': [],
+ 'DEPLOY_COMMANDS': {'default': []},
'DISABLED_PLUGINS': [],
'EXTRA_PLUGINS_DIRS': [],
'COMMENT_SYSTEM_ID': 'nikolademo',
'EXTRA_HEAD_DATA': '',
- 'FAVICONS': {},
+ 'FAVICONS': (),
'FEED_LENGTH': 10,
'FILE_METADATA_REGEXP': None,
'ADDITIONAL_METADATA': {},
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
'FORCE_ISO8601': False,
- 'GALLERY_PATH': 'galleries',
+ 'GALLERY_FOLDERS': {'galleries': 'galleries'},
'GALLERY_SORT_BY_DATE': True,
+ 'GLOBAL_CONTEXT_FILLER': [],
'GZIP_COMMAND': None,
'GZIP_FILES': False,
'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'),
+ 'HIDDEN_TAGS': [],
+ 'HIDDEN_CATEGORIES': [],
'HYPHENATE': False,
+ 'IMAGE_FOLDERS': {'images': ''},
'INDEX_DISPLAY_POST_COUNT': 10,
'INDEX_FILE': 'index.html',
'INDEX_TEASERS': False,
+ 'IMAGE_THUMBNAIL_SIZE': 400,
'INDEXES_TITLE': "",
'INDEXES_PAGES': "",
'INDEXES_PAGES_MAIN': False,
+ 'INDEXES_PRETTY_PAGE_URL': False,
+ 'INDEXES_STATIC': True,
'INDEX_PATH': '',
'IPYNB_CONFIG': {},
'LESS_COMPILER': 'lessc',
'LESS_OPTIONS': [],
'LICENSE': '',
'LINK_CHECK_WHITELIST': [],
- 'LISTINGS_FOLDER': 'listings',
+ 'LISTINGS_FOLDERS': {'listings': 'listings'},
'LOGO_URL': '',
'NAVIGATION_LINKS': {},
- 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'],
+ 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'], # FIXME: Add 'extras' in v8
'MAX_IMAGE_SIZE': 1280,
'MATHJAX_CONFIG': '',
'OLD_THEME_SUPPORT': True,
'OUTPUT_FOLDER': 'output',
'POSTS': (("posts/*.txt", "posts", "post.tmpl"),),
'PAGES': (("stories/*.txt", "stories", "story.tmpl"),),
+ 'PANDOC_OPTIONS': [],
'PRETTY_URLS': False,
'FUTURE_IS_NOW': False,
'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK,
'RSS_READ_MORE_LINK': DEFAULT_RSS_READ_MORE_LINK,
+ 'RSS_LINKS_APPEND_QUERY': False,
'REDIRECTIONS': [],
'ROBOTS_EXCLUSIONS': [],
+ 'GENERATE_ATOM': False,
'GENERATE_RSS': True,
'RSS_LINK': None,
'RSS_PATH': '',
@@ -312,13 +412,15 @@ class Nikola(object):
'SHOW_SOURCELINK': True,
'SHOW_UNTRANSLATED_POSTS': True,
'SLUG_TAG_PATH': True,
- 'SOCIAL_BUTTONS_CODE': SOCIAL_BUTTONS_CODE,
- 'SITE_URL': 'http://getnikola.com/',
+ 'SOCIAL_BUTTONS_CODE': '',
+ 'SITE_URL': 'https://example.com/',
'STORY_INDEX': False,
'STRIP_INDEXES': False,
'SITEMAP_INCLUDE_FILELESS_DIRS': True,
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
+ 'TAG_PAGES_DESCRIPTIONS': {},
+ 'TAGLIST_MINIMUM_POSTS': 1,
'TEMPLATE_FILTERS': {},
'THEME': 'bootstrap',
'THEME_REVEAL_CONFIG_SUBTHEME': 'sky',
@@ -328,16 +430,21 @@ class Nikola(object):
'URL_TYPE': 'rel_path',
'USE_BUNDLES': True,
'USE_CDN': False,
+ 'USE_CDN_WARNING': True,
'USE_FILENAME_AS_TITLE': True,
'USE_OPEN_GRAPH': True,
'USE_SLUGIFY': True,
'TIMEZONE': 'UTC',
+ 'WRITE_TAG_CLOUD': 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',
}
# set global_context for template rendering
@@ -376,7 +483,10 @@ class Nikola(object):
'EXTRA_HEAD_DATA',
'NAVIGATION_LINKS',
'INDEX_READ_MORE_LINK',
- 'RSS_READ_MORE_LINK',)
+ 'RSS_READ_MORE_LINK',
+ 'INDEXES_TITLE',
+ 'INDEXES_PAGES',
+ 'INDEXES_PRETTY_PAGE_URL',)
self._GLOBAL_CONTEXT_TRANSLATABLE = ('blog_author',
'blog_title',
@@ -478,28 +588,77 @@ class Nikola(object):
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
+ if self.config['CATEGORY_PATH'] is None:
+ self.config['CATEGORY_PATH'] = self.config['TAG_PATH']
+ if self.config['CATEGORY_PAGES_ARE_INDEXES'] is None:
+ self.config['CATEGORY_PAGES_ARE_INDEXES'] = self.config['TAG_PAGES_ARE_INDEXES']
+
self.default_lang = self.config['DEFAULT_LANG']
self.translations = self.config['TRANSLATIONS']
- if self.configured:
- locale_fallback, locale_default, locales = sanitized_locales(
- self.config.get('LOCALE_FALLBACK', None),
- self.config.get('LOCALE_DEFAULT', None),
- self.config.get('LOCALES', {}), self.translations)
- utils.LocaleBorg.initialize(locales, self.default_lang)
+ locale_fallback, locale_default, locales = sanitized_locales(
+ self.config.get('LOCALE_FALLBACK', None),
+ self.config.get('LOCALE_DEFAULT', None),
+ self.config.get('LOCALES', {}), self.translations)
+ utils.LocaleBorg.initialize(locales, self.default_lang)
# BASE_URL defaults to SITE_URL
if 'BASE_URL' not in self.config:
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.")
+ utils.LOGGER.warn("Your BASE_URL doesn't end in / -- adding it, but please fix it in your config file!")
+ self.config['BASE_URL'] += '/'
+
+ try:
+ _bnl = urlsplit(self.config['BASE_URL']).netloc
+ _bnl.encode('ascii')
+ urlsplit(self.config['SITE_URL']).netloc.encode('ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ utils.LOGGER.error("Your BASE_URL or SITE_URL contains an IDN expressed in Unicode. Please convert it to Punycode.")
+ 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.")
# We use one global tzinfo object all over Nikola.
- self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])
+ try:
+ self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])
+ except Exception as exc:
+ utils.LOGGER.warn("Error getting TZ: {}", exc)
+ self.tzinfo = dateutil.tz.gettz()
self.config['__tzinfo__'] = self.tzinfo
self.plugin_manager = PluginManager(categories_filter={
@@ -512,6 +671,8 @@ class Nikola(object):
"RestExtension": RestExtension,
"MarkdownExtension": MarkdownExtension,
"SignalHandler": SignalHandler,
+ "ConfigPlugin": ConfigPlugin,
+ "PostScanner": PostScanner,
})
self.plugin_manager.setPluginInfoExtension('plugin')
extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS']
@@ -531,45 +692,27 @@ class Nikola(object):
self.plugin_manager.setPluginPlaces(places)
self.plugin_manager.collectPlugins()
- # Activate all required SignalHandler plugins
- for plugin_info in self.plugin_manager.getPluginsOfCategory("SignalHandler"):
- if plugin_info.name in self.config.get('DISABLED_PLUGINS'):
- self.plugin_manager.removePluginFromCategory(plugin_info, "SignalHandler")
- else:
- self.plugin_manager.activatePluginByName(plugin_info.name)
- plugin_info.plugin_object.set_site(self)
+ self._activate_plugins_of_category("SignalHandler")
# Emit signal for SignalHandlers which need to start running immediately.
signal('sighandlers_loaded').send(self)
self._commands = {}
- # Activate all command plugins
- for plugin_info in self.plugin_manager.getPluginsOfCategory("Command"):
- if plugin_info.name in self.config['DISABLED_PLUGINS']:
- self.plugin_manager.removePluginFromCategory(plugin_info, "Command")
- continue
-
- self.plugin_manager.activatePluginByName(plugin_info.name)
- plugin_info.plugin_object.set_site(self)
+
+ command_plugins = self._activate_plugins_of_category("Command")
+ for plugin_info in command_plugins:
plugin_info.plugin_object.short_help = plugin_info.description
self._commands[plugin_info.name] = plugin_info.plugin_object
- # Activate all task plugins
- for task_type in ["Task", "LateTask"]:
- for plugin_info in self.plugin_manager.getPluginsOfCategory(task_type):
- if plugin_info.name in self.config['DISABLED_PLUGINS']:
- self.plugin_manager.removePluginFromCategory(plugin_info, task_type)
- continue
- self.plugin_manager.activatePluginByName(plugin_info.name)
- plugin_info.plugin_object.set_site(self)
+ self._activate_plugins_of_category("PostScanner")
+ self._activate_plugins_of_category("Task")
+ self._activate_plugins_of_category("LateTask")
+ self._activate_plugins_of_category("TaskMultiplier")
- # Activate all multiplier plugins
- for plugin_info in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"):
- if plugin_info.name in self.config['DISABLED_PLUGINS']:
- self.plugin_manager.removePluginFromCategory(plugin_info, task_type)
- continue
- self.plugin_manager.activatePluginByName(plugin_info.name)
- plugin_info.plugin_object.set_site(self)
+ # Store raw compilers for internal use (need a copy for that)
+ self.config['_COMPILERS_RAW'] = {}
+ for k, v in self.config['COMPILERS'].items():
+ self.config['_COMPILERS_RAW'][k] = list(v)
compilers = defaultdict(set)
# Also add aliases for combinations with TRANSLATIONS_PATTERN
@@ -604,11 +747,11 @@ class Nikola(object):
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_bundles'] = self.config['USE_BUNDLES']
self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
- self._GLOBAL_CONTEXT['date_format'] = self.config.get(
- 'DATE_FORMAT', '%Y-%m-%d %H:%M')
+ 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['show_blog_title'] = self.config.get('SHOW_BLOG_TITLE')
@@ -634,6 +777,7 @@ class Nikola(object):
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')
@@ -649,9 +793,24 @@ class Nikola(object):
self._GLOBAL_CONTEXT['show_sourcelink'] = self.config.get(
'SHOW_SOURCELINK')
self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
+ self._GLOBAL_CONTEXT['date_fanciness'] = self.config.get('DATE_FANCINESS')
+ self._GLOBAL_CONTEXT['js_date_format'] = json.dumps(self.config.get('JS_DATE_FORMAT'))
self._GLOBAL_CONTEXT['colorbox_locales'] = LEGAL_VALUES['COLORBOX_LOCALES']
+ self._GLOBAL_CONTEXT['momentjs_locales'] = LEGAL_VALUES['MOMENTJS_LOCALES']
+ self._GLOBAL_CONTEXT['hidden_tags'] = self.config.get('HIDDEN_TAGS')
+ self._GLOBAL_CONTEXT['hidden_categories'] = self.config.get('HIDDEN_CATEGORIES')
self._GLOBAL_CONTEXT['url_replacer'] = self.url_replacer
+ # IPython theme configuration. If a website can potentially have ipynb
+ # posts (as determined by checking POSTS/PAGES against ipynb
+ # extensions), we should enable the IPython CSS (leaving that up to the
+ # theme itself).
+
+ self._GLOBAL_CONTEXT['needs_ipython_css'] = False
+ for i in self.config['post_pages']:
+ if os.path.splitext(i[0])[1] in self.config['COMPILERS'].get('ipynb', []):
+ self._GLOBAL_CONTEXT['needs_ipython_css'] = True
+
self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
# Load compiler plugins
@@ -663,8 +822,22 @@ class Nikola(object):
self.compilers[plugin_info.name] = \
plugin_info.plugin_object
+ self._activate_plugins_of_category("ConfigPlugin")
+
signal('configured').send(self)
+ def _activate_plugins_of_category(self, category):
+ """Activate all the plugins of a given category and return them."""
+ plugins = []
+ for plugin_info in self.plugin_manager.getPluginsOfCategory(category):
+ if plugin_info.name in self.config.get('DISABLED_PLUGINS'):
+ self.plugin_manager.removePluginFromCategory(plugin_info, category)
+ else:
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
+ plugins.append(plugin_info)
+ return plugins
+
def _get_themes(self):
if self._THEMES is None:
try:
@@ -674,7 +847,7 @@ class Nikola(object):
self.config['THEME'] = 'bootstrap'
return self._get_themes()
# Check consistency of USE_CDN and the current THEME (Issue #386)
- if self.config['USE_CDN']:
+ 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']:
@@ -763,12 +936,23 @@ class Nikola(object):
"handle '{0}' extensions.".format(ext))
lang = langs[0]
- compile_html = self.compilers[lang]
+ try:
+ compile_html = 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
return compile_html
def render_template(self, template_name, output_name, context):
+ """Render a template with the global context.
+
+ If ``output_name`` is None, will return a string and all URL
+ normalization will be ignored (including the link:// scheme).
+ If ``output_name`` is a string, URLs will be normalized and
+ the resultant HTML will be saved to the named file (path must
+ start with OUTPUT_FOLDER).
+ """
local_context = {}
local_context["template_name"] = template_name
local_context.update(self.GLOBAL_CONTEXT)
@@ -781,9 +965,15 @@ class Nikola(object):
for h in local_context['template_hooks'].values():
h.context = context
+ for func in self.config['GLOBAL_CONTEXT_FILLER']:
+ func(local_context, template_name)
+
data = self.template_system.render_template(
template_name, None, local_context)
+ if output_name is None:
+ return data
+
assert output_name.startswith(
self.config["OUTPUT_FOLDER"])
url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:]
@@ -796,13 +986,14 @@ class Nikola(object):
src = "/".join(src.split(os.sep))
utils.makedirs(os.path.dirname(output_name))
- doc = lxml.html.document_fromstring(data)
+ parser = lxml.html.HTMLParser(remove_blank_text=True)
+ doc = lxml.html.document_fromstring(data, parser)
doc.rewrite_links(lambda dst: self.url_replacer(src, dst, context['lang']))
data = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True)
with open(output_name, "wb+") as post_file:
post_file.write(data)
- def url_replacer(self, src, dst, lang=None):
+ def url_replacer(self, src, dst, lang=None, url_type=None):
"""URL mangler.
* Replaces link:// URLs with real links
@@ -814,19 +1005,42 @@ class Nikola(object):
src is the URL where this link is used
dst is the link to be mangled
lang is used for language-sensitive URLs in link://
-
+ url_type is used to determine final link appearance, defaulting to URL_TYPE from config
"""
parsed_src = urlsplit(src)
src_elems = parsed_src.path.split('/')[1:]
dst_url = urlparse(dst)
if lang is None:
lang = self.default_lang
+ if url_type is None:
+ url_type = self.config.get('URL_TYPE')
+
+ if dst_url.scheme and dst_url.scheme not in ['http', 'https', 'link']:
+ return dst
# 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)
+ # Assuming the site is served over one of these, and
+ # since those are the only URLs we want to rewrite...
else:
+ if '%' in dst_url.netloc:
+ # convert lxml percent-encoded garbage to punycode
+ nl = unquote(dst_url.netloc)
+ try:
+ nl = nl.decode('utf-8')
+ except AttributeError:
+ # python 3: already unicode
+ pass
+ nl = nl.encode('idna')
+ if isinstance(nl, utils.bytes_str):
+ nl = nl.decode('latin-1') # so idna stays unchanged
+ dst = urlunsplit((dst_url.scheme,
+ nl,
+ dst_url.path,
+ dst_url.query,
+ dst_url.fragment))
return dst
elif dst_url.scheme == 'link': # Magic absolute path link:
dst = dst_url.path
@@ -843,10 +1057,10 @@ class Nikola(object):
# Avoid empty links.
if src == dst:
- if self.config.get('URL_TYPE') == 'absolute':
+ if url_type == 'absolute':
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
return dst
- elif self.config.get('URL_TYPE') == 'full_path':
+ elif url_type == 'full_path':
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
return urlparse(dst).path
else:
@@ -855,13 +1069,13 @@ class Nikola(object):
# Check that link can be made relative, otherwise return dest
parsed_dst = urlsplit(dst)
if parsed_src[:2] != parsed_dst[:2]:
- if self.config.get('URL_TYPE') == 'absolute':
+ if url_type == 'absolute':
dst = urljoin(self.config['BASE_URL'], dst)
return dst
- if self.config.get('URL_TYPE') in ('full_path', 'absolute'):
+ if url_type in ('full_path', 'absolute'):
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
- if self.config.get('URL_TYPE') == 'full_path':
+ 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)
@@ -895,31 +1109,41 @@ class Nikola(object):
return result
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_teasers, rss_plain, feed_length=10, feed_url=None,
+ enclosure=_enclosure, rss_links_append_query=None):
"""Takes all necessary data, and renders a RSS feed in output_path."""
- rss_obj = rss.RSS2(
+ rss_obj = utils.ExtendedRSS2(
title=title,
link=link,
description=description,
- lastBuildDate=datetime.datetime.now(),
+ lastBuildDate=datetime.datetime.utcnow(),
generator='http://getnikola.com/',
language=lang
)
+ if feed_url:
+ absurl = '/' + feed_url[len(self.config['BASE_URL']):]
+ rss_obj.xsl_stylesheet_href = self.url_replacer(absurl, "/assets/xml/rss.xsl")
+
items = []
+ feed_append_query = None
+ if rss_links_append_query:
+ feed_append_query = rss_links_append_query.format(
+ feedRelUri='/' + feed_url[len(self.config['BASE_URL']):],
+ feedFormat="rss")
+
for post in timeline[:feed_length]:
- old_url_type = self.config['URL_TYPE']
- self.config['URL_TYPE'] = 'absolute'
- data = post.text(lang, teaser_only=rss_teasers, strip_html=rss_plain, rss_read_more_link=True)
+ data = post.text(lang, teaser_only=rss_teasers, strip_html=rss_plain,
+ rss_read_more_link=True, rss_links_append_query=feed_append_query)
if feed_url is not None and data:
# Massage the post's HTML (unless plain)
if not rss_plain:
# FIXME: this is duplicated with code in Post.text()
try:
doc = lxml.html.document_fromstring(data)
- doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang))
+ doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang, 'absolute'))
try:
body = doc.body
data = (body.text or '') + ''.join(
@@ -932,32 +1156,33 @@ class Nikola(object):
data = ""
else: # let other errors raise
raise(e)
- self.config['URL_TYPE'] = old_url_type
args = {
'title': post.title(lang),
- 'link': post.permalink(lang, absolute=True),
+ 'link': post.permalink(lang, absolute=True, query=feed_append_query),
'description': data,
- 'guid': post.permalink(lang, absolute=True),
# PyRSS2Gen's pubDate is GMT time.
'pubDate': (post.date if post.date.tzinfo is None else
post.date.astimezone(dateutil.tz.tzutc())),
'categories': post._tags.get(lang, []),
'creator': post.author(lang),
+ 'guid': post.permalink(lang, absolute=True),
}
if post.author(lang):
rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
- """ Enclosure callback must returns tuple """
- # enclosure callback returns None if post has no enclosure, or a
- # 3-tuple of (url, length (0 is valid), mimetype)
- enclosure_details = enclosure(post=post, lang=lang)
- if enclosure_details is not None:
- args['enclosure'] = rss.Enclosure(*enclosure_details)
+ if enclosure:
+ # enclosure callback returns None if post has no enclosure, or a
+ # 3-tuple of (url, length (0 is valid), mimetype)
+ enclosure_details = enclosure(post=post, lang=lang)
+ if enclosure_details is not None:
+ args['enclosure'] = rss.Enclosure(*enclosure_details)
items.append(utils.ExtendedItem(**args))
rss_obj.items = items
+ rss_obj.self_url = feed_url
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
dst_dir = os.path.dirname(output_path)
utils.makedirs(dst_dir)
@@ -970,12 +1195,13 @@ class Nikola(object):
def path(self, kind, name, lang=None, is_link=False):
"""Build the path to a certain kind of page.
- These are mostly defined by plugins by registering via
- the register_path_handler method, except for slug and
- post_path which are defined in this class' init method.
+ These are mostly defined by plugins by registering via the
+ register_path_handler method, except for slug, post_path, root
+ and filename which are defined in this class' init method.
Here's some of the others, for historical reasons:
+ * root (name is ignored)
* tag_index (name is ignored)
* tag (and name is the tag name)
* tag_rss (name is the tag name)
@@ -1027,6 +1253,14 @@ class Nikola(object):
os.path.dirname(name),
self.config['INDEX_FILE']] if _f]
+ def root_path(self, name, lang):
+ """root_path path handler"""
+ d = self.config['TRANSLATIONS'][lang]
+ if d:
+ return [d, '']
+ else:
+ return []
+
def slug_path(self, name, lang):
"""slug path handler"""
results = [p for p in self.timeline if p.meta('slug') == name]
@@ -1121,6 +1355,9 @@ class Nikola(object):
for task in flatten(pluginInfo.plugin_object.gen_tasks()):
assert 'basename' in task
task = self.clean_task_paths(task)
+ if 'task_dep' not in task:
+ task['task_dep'] = []
+ task['task_dep'].extend(self.injected_deps[task['basename']])
yield task
for multi in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"):
flag = False
@@ -1139,108 +1376,132 @@ class Nikola(object):
'task_dep': task_dep
}
- def scan_posts(self, really=False):
- """Scan all the posts."""
+ def parse_category_name(self, category_name):
+ if self.config['CATEGORY_ALLOW_HIERARCHIES']:
+ try:
+ return utils.parse_escaped_hierarchical_category_name(category_name)
+ except Exception as e:
+ utils.LOGGER.error(str(e))
+ sys.exit(1)
+ else:
+ return [category_name] if len(category_name) > 0 else []
+
+ def category_path_to_category_name(self, category_path):
+ if self.config['CATEGORY_ALLOW_HIERARCHIES']:
+ return utils.join_hierarchical_category_path(category_path)
+ else:
+ return ''.join(category_path)
+
+ def _add_post_to_category(self, post, category_name):
+ category_path = self.parse_category_name(category_name)
+ current_path = []
+ current_subtree = self.category_hierarchy
+ for current in category_path:
+ current_path.append(current)
+ if current not in current_subtree:
+ current_subtree[current] = {}
+ current_subtree = current_subtree[current]
+ self.posts_per_category[self.category_path_to_category_name(current_path)].append(post)
+
+ def _sort_category_hierarchy(self):
+ # First create a hierarchy of TreeNodes
+ self.category_hierarchy_lookup = {}
+
+ def create_hierarchy(cat_hierarchy, parent=None):
+ result = []
+ for name, children in cat_hierarchy.items():
+ node = 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)
+ self.category_hierarchy_lookup[node.category_name] = node
+ if node.category_name not in self.config.get('HIDDEN_CATEGORIES'):
+ result.append(node)
+ return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC)
+
+ root_list = create_hierarchy(self.category_hierarchy)
+ # Next, flatten the hierarchy
+ self.category_hierarchy = utils.flatten_tree_structure(root_list)
+
+ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
+ """Scan all the posts.
+
+ Ignoring quiet.
+ """
if self._scanned and not really:
return
- self.commands = utils.Commands(self.doit)
- self.global_data = {}
+ # Reset things
self.posts = []
+ self.all_posts = []
self.posts_per_year = defaultdict(list)
self.posts_per_month = defaultdict(list)
self.posts_per_tag = defaultdict(list)
self.posts_per_category = defaultdict(list)
+ self.category_hierarchy = {}
self.post_per_file = {}
self.timeline = []
self.pages = []
- seen = set([])
- if not self.quiet:
- print("Scanning posts", end='', file=sys.stderr)
- slugged_tags = set([])
+ for p in self.plugin_manager.getPluginsOfCategory('PostScanner'):
+ timeline = p.plugin_object.scan()
+ # FIXME: can there be conflicts here?
+ self.timeline.extend(timeline)
+
quit = False
- for wildcard, destination, template_name, use_in_feeds in \
- self.config['post_pages']:
- if not self.quiet:
- print(".", end='', file=sys.stderr)
- 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/
- # Get all the untranslated paths
- dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst
- untranslated = glob.glob(dir_glob)
- # And now get all the translated paths
- translated = set([])
- for lang in self.config['TRANSLATIONS'].keys():
- if lang == self.config['DEFAULT_LANG']:
- continue
- lang_glob = utils.get_translation_candidate(self.config, dir_glob, lang) # posts/foo/*.LANG.rst
- translated = translated.union(set(glob.glob(lang_glob)))
- # untranslated globs like *.rst often match translated paths too, so remove them
- # and ensure x.rst is not in the translated set
- untranslated = set(untranslated) - translated
-
- # also remove from translated paths that are translations of
- # paths in untranslated_list, so x.es.rst is not in the untranslated set
- for p in untranslated:
- translated = translated - set([utils.get_translation_candidate(self.config, p, l) for l in self.config['TRANSLATIONS'].keys()])
-
- full_list = list(translated) + list(untranslated)
- # We eliminate from the list the files inside any .ipynb folder
- full_list = [p for p in full_list
- if not any([x.startswith('.')
- for x in p.split(os.sep)])]
-
- for base_path in full_list:
- if base_path in seen:
- continue
- else:
- seen.add(base_path)
- post = Post(
- base_path,
- self.config,
- dest_dir,
- use_in_feeds,
- self.MESSAGES,
- template_name,
- self.get_compiler(base_path)
- )
- self.timeline.append(post)
- self.global_data[post.source_path] = post
- if post.use_in_feeds:
- self.posts.append(post)
- self.posts_per_year[
- str(post.date.year)].append(post)
- self.posts_per_month[
- '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post)
- for tag in post.alltags:
- _tag_slugified = utils.slugify(tag)
- if _tag_slugified in slugged_tags:
- 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) == _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.add(utils.slugify(tag, force=True))
- self.posts_per_tag[tag].append(post)
- self.posts_per_category[post.meta('category')].append(post)
+ # Classify posts per year/tag/month/whatever
+ slugged_tags = set([])
+ for post in self.timeline:
+ if post.use_in_feeds:
+ self.posts.append(post)
+ self.posts_per_year[str(post.date.year)].append(post)
+ self.posts_per_month[
+ '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post)
+ for tag in post.alltags:
+ _tag_slugified = utils.slugify(tag)
+ if _tag_slugified in slugged_tags:
+ 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) == _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:
- self.pages.append(post)
- self.post_per_file[post.destination_path(lang=lang)] = post
- self.post_per_file[post.destination_path(lang=lang, extension=post.source_ext())] = post
+ slugged_tags.add(utils.slugify(tag, force=True))
+ self.posts_per_tag[tag].append(post)
+ self._add_post_to_category(post, post.meta('category'))
+
+ if post.is_post:
+ # unpublished posts
+ self.all_posts.append(post)
+ else:
+ self.pages.append(post)
+
+ for lang in self.config['TRANSLATIONS'].keys():
+ dest = post.destination_path(lang=lang)
+ src_dest = post.destination_path(lang=lang, extension=post.source_ext())
+ if dest in self.post_per_file:
+ utils.LOGGER.error('Two posts are trying to generate {0}: {1} and {2}'.format(
+ dest,
+ self.post_per_file[dest].source_path,
+ post.source_path))
+ quit = True
+ if (src_dest in self.post_per_file) and self.config['COPY_SOURCES']:
+ utils.LOGGER.error('Two posts are trying to generate {0}: {1} and {2}'.format(
+ src_dest,
+ self.post_per_file[dest].source_path,
+ post.source_path))
+ quit = True
+ self.post_per_file[dest] = post
+ self.post_per_file[src_dest] = post
# Sort everything.
- self.timeline.sort(key=lambda p: p.date)
- self.timeline.reverse()
- self.posts.sort(key=lambda p: p.date)
- self.posts.reverse()
- self.pages.sort(key=lambda p: p.date)
- self.pages.reverse()
+
+ for thing in self.timeline, self.posts, self.all_posts, self.pages:
+ thing.sort(key=lambda p: (p.date, p.source_path))
+ thing.reverse()
+ self._sort_category_hierarchy()
for i, p in enumerate(self.posts[1:]):
p.next_post = self.posts[i]
@@ -1249,11 +1510,9 @@ class Nikola(object):
self._scanned = True
if not self.quiet:
print("done!", file=sys.stderr)
-
- signal('scanned').send(self)
-
- if quit:
+ if quit and not ignore_quit:
sys.exit(1)
+ signal('scanned').send(self)
def generic_page_renderer(self, lang, post, filters):
"""Render post fragments to final HTML pages."""
@@ -1303,7 +1562,7 @@ class Nikola(object):
'actions': [(self.render_template, [post.template_name,
output_name, context])],
'clean': True,
- 'uptodate': [config_changed(deps_dict)],
+ 'uptodate': [config_changed(deps_dict, 'nikola.nikola.Nikola.generic_page_renderer')] + post.deps_uptodate(lang),
}
yield utils.apply_filters(task, filters)
@@ -1313,8 +1572,10 @@ class Nikola(object):
"""Renders pages with lists of posts."""
deps = self.template_system.template_deps(template_name)
+ uptodate_deps = []
for post in posts:
deps += post.deps(lang)
+ uptodate_deps += post.deps_uptodate(lang)
context = {}
context["posts"] = posts
context["title"] = self.config['BLOG_TITLE'](lang)
@@ -1343,11 +1604,314 @@ class Nikola(object):
'actions': [(self.render_template, [template_name, output_name,
context])],
'clean': True,
- 'uptodate': [config_changed(deps_context)]
+ 'uptodate': [config_changed(deps_context, 'nikola.nikola.Nikola.generic_post_list_renderer')] + uptodate_deps
}
return utils.apply_filters(task, filters)
+ def atom_feed_renderer(self, lang, posts, output_path, filters,
+ extra_context):
+ """Renders Atom feeds and archives with lists of posts. Feeds are
+ considered archives when no future updates to them are expected"""
+
+ def atom_link(link_rel, link_type, link_href):
+ link = lxml.etree.Element("link")
+ link.set("rel", link_rel)
+ link.set("type", link_type)
+ link.set("href", link_href)
+ return link
+
+ deps = []
+ uptodate_deps = []
+ for post in posts:
+ deps += post.deps(lang)
+ uptodate_deps += post.deps_uptodate(lang)
+ context = {}
+ context["posts"] = posts
+ context["title"] = self.config['BLOG_TITLE'](lang)
+ 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)
+ deps_context = copy(context)
+ deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in
+ posts]
+ deps_context["global"] = self.GLOBAL_CONTEXT
+
+ for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
+ deps_context[k] = deps_context['global'][k](lang)
+
+ deps_context['navigation_links'] = deps_context['global']['navigation_links'](lang)
+
+ nslist = {}
+ if context["is_feed_stale"] or (not context["feedpagenum"] == context["feedpagecount"] - 1 and not context["feedpagenum"] == 0):
+ nslist["fh"] = "http://purl.org/syndication/history/1.0"
+ if not self.config["RSS_TEASERS"]:
+ nslist["xh"] = "http://www.w3.org/1999/xhtml"
+ feed_xsl_link = self.abs_link("/assets/xml/atom.xsl")
+ feed_root = lxml.etree.Element("feed", nsmap=nslist)
+ feed_root.addprevious(lxml.etree.ProcessingInstruction(
+ "xml-stylesheet",
+ 'href="' + feed_xsl_link + '" type="text/xsl media="all"'))
+ feed_root.set("{http://www.w3.org/XML/1998/namespace}lang", lang)
+ feed_root.set("xmlns", "http://www.w3.org/2005/Atom")
+ feed_title = lxml.etree.SubElement(feed_root, "title")
+ feed_title.text = context["title"]
+ feed_id = lxml.etree.SubElement(feed_root, "id")
+ feed_id.text = self.abs_link(context["feedlink"])
+ feed_updated = lxml.etree.SubElement(feed_root, "updated")
+ feed_updated.text = datetime.datetime.now(tz=dateutil.tz.tzutc()).replace(microsecond=0).isoformat()
+ feed_author = lxml.etree.SubElement(feed_root, "author")
+ feed_author_name = lxml.etree.SubElement(feed_author, "name")
+ feed_author_name.text = self.config["BLOG_AUTHOR"](lang)
+ 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 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 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 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")
+ feed_generator.set("uri", "http://getnikola.com/")
+ feed_generator.text = "Nikola"
+
+ feed_append_query = None
+ if self.config["RSS_LINKS_APPEND_QUERY"]:
+ feed_append_query = self.config["RSS_LINKS_APPEND_QUERY"].format(
+ feedRelUri=context["feedlink"],
+ feedFormat="atom")
+
+ for post in posts:
+ data = post.text(lang, teaser_only=self.config["RSS_TEASERS"], strip_html=self.config["RSS_TEASERS"],
+ rss_read_more_link=True, rss_links_append_query=feed_append_query)
+ if not self.config["RSS_TEASERS"]:
+ # FIXME: this is duplicated with code in Post.text() and generic_rss_renderer
+ try:
+ doc = lxml.html.document_fromstring(data)
+ doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang, 'absolute'))
+ try:
+ body = doc.body
+ data = (body.text or '') + ''.join(
+ [lxml.html.tostring(child, encoding='unicode')
+ for child in body.iterchildren()])
+ except IndexError: # No body there, it happens sometimes
+ data = ''
+ except lxml.etree.ParserError as e:
+ if str(e) == "Document is empty":
+ data = ""
+ else: # let other errors raise
+ raise(e)
+
+ entry_root = lxml.etree.SubElement(feed_root, "entry")
+ entry_title = lxml.etree.SubElement(entry_root, "title")
+ entry_title.text = post.title(lang)
+ entry_id = lxml.etree.SubElement(entry_root, "id")
+ entry_id.text = post.permalink(lang, absolute=True)
+ entry_updated = lxml.etree.SubElement(entry_root, "updated")
+ entry_updated.text = post.updated.isoformat()
+ entry_published = lxml.etree.SubElement(entry_root, "published")
+ entry_published.text = post.date.isoformat()
+ entry_author = lxml.etree.SubElement(entry_root, "author")
+ 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)))
+ if self.config["RSS_TEASERS"]:
+ entry_summary = lxml.etree.SubElement(entry_root, "summary")
+ entry_summary.text = data
+ else:
+ entry_content = lxml.etree.SubElement(entry_root, "content")
+ entry_content.set("type", "xhtml")
+ entry_content_nsdiv = lxml.etree.SubElement(entry_content, "{http://www.w3.org/1999/xhtml}div")
+ entry_content_nsdiv.text = data
+ for category in post.tags:
+ entry_category = lxml.etree.SubElement(entry_root, "category")
+ entry_category.set("term", utils.slugify(category))
+ entry_category.set("label", category)
+
+ dst_dir = os.path.dirname(output_path)
+ 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):
+ 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=[]):
+ """Creates an index page.
+
+ lang: The language
+ posts: A list of posts
+ indexes_title: Title
+ template_name: Name of template file
+ 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
+ page_link: A function accepting an index i, the displayed page number,
+ the number of pages, and a boolean force_addition
+ which creates a link to the i-th page (where i ranges
+ between 0 and num_pages-1). The displayed page (between 1
+ and num_pages) is the number (optionally) displayed as
+ 'page %d' on the rendered page. If force_addition is True,
+ the appendum (inserting '-%d' etc.) should be done also for
+ i == 0.
+ page_path: A function accepting an index i, the displayed page number,
+ the number of pages, and a boolean force_addition,
+ which creates a path to the i-th page. All arguments are
+ as the ones for page_link.
+ additional_dependencies: a list of dependencies which will be added
+ to task['uptodate']
+ """
+ # Update kw
+ kw = kw.copy()
+ kw["tag_pages_are_indexes"] = self.config['TAG_PAGES_ARE_INDEXES']
+ kw["index_display_post_count"] = self.config['INDEX_DISPLAY_POST_COUNT']
+ kw["index_teasers"] = self.config['INDEX_TEASERS']
+ 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["RSS_LINKS_APPEND_QUERY"]
+ kw['feed_teasers'] = self.config["RSS_TEASERS"]
+ kw['currentfeed'] = None
+
+ # Split in smaller lists
+ lists = []
+ if kw["indexes_static"]:
+ lists.append(posts[:kw["index_display_post_count"]])
+ posts = posts[kw["index_display_post_count"]:]
+ while posts:
+ lists.append(posts[-kw["index_display_post_count"]:])
+ posts = posts[:-kw["index_display_post_count"]]
+ else:
+ while posts:
+ lists.append(posts[:kw["index_display_post_count"]])
+ posts = posts[kw["index_display_post_count"]:]
+ num_pages = len(lists)
+ for i, post_list in enumerate(lists):
+ context = context_source.copy()
+ ipages_i = utils.get_displayed_page_number(i, num_pages, self)
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % ipages_i
+ else:
+ if kw["indexes_pages_main"]:
+ ipages_msg = "page %d"
+ else:
+ ipages_msg = "old posts, page %d"
+ indexes_pages = " (" + \
+ kw["messages"][lang][ipages_msg] % ipages_i + ")"
+ if i > 0 or kw["indexes_pages_main"]:
+ context["title"] = indexes_title + indexes_pages
+ else:
+ context["title"] = indexes_title
+ context["prevlink"] = None
+ context["nextlink"] = None
+ context['index_teasers'] = kw['index_teasers']
+ prevlink = None
+ nextlink = None
+ if kw["indexes_static"]:
+ if i > 0:
+ if i < num_pages - 1:
+ prevlink = i + 1
+ elif i == num_pages - 1:
+ prevlink = 0
+ if num_pages > 1:
+ if i > 1:
+ nextlink = i - 1
+ elif i == 0:
+ nextlink = num_pages - 1
+ else:
+ if i >= 1:
+ prevlink = i - 1
+ 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),
+ 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),
+ num_pages, False, extension=".atom")
+ context["permalink"] = page_link(i, ipages_i, num_pages, False)
+ output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False))
+ task = self.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.nikola.Nikola.generic_index_renderer')] + additional_dependencies
+ 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
+ atom_task = {
+ "basename": basename,
+ "file_dep": [output_name],
+ "name": atom_output_name,
+ "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):
+ # 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)
+ yield utils.apply_filters({
+ 'basename': basename,
+ 'name': output_name,
+ 'targets': [output_name],
+ 'actions': [(utils.create_redirect, (output_name, link))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw, 'nikola.nikola.Nikola.generic_index_renderer')],
+ }, kw["filters"])
+
def __repr__(self):
return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE']())
@@ -1526,18 +2090,3 @@ _windows_locale_guesses = {
"tr_tr": "Turkish",
"zh_cn": "Chinese_China", # Chinese (Simplified)
}
-
-
-SOCIAL_BUTTONS_CODE = """
-<!-- Social buttons -->
-<div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
-<a class="addthis_button_more">Share</a>
-<ul><li><a class="addthis_button_facebook"></a>
-<li><a class="addthis_button_google_plusone_share"></a>
-<li><a class="addthis_button_linkedin"></a>
-<li><a class="addthis_button_twitter"></a>
-</ul>
-</div>
-<script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
-<!-- End of social buttons -->
-"""
diff --git a/nikola/packages/__init__.py b/nikola/packages/__init__.py
index e69de29..0a704f5 100644
--- a/nikola/packages/__init__.py
+++ b/nikola/packages/__init__.py
@@ -0,0 +1 @@
+# Packages vendored in by Nikola.
diff --git a/nikola/packages/tzlocal/darwin.py b/nikola/packages/tzlocal/darwin.py
index 8aeee51..8bfe807 100644
--- a/nikola/packages/tzlocal/darwin.py
+++ b/nikola/packages/tzlocal/darwin.py
@@ -1,17 +1,26 @@
from __future__ import with_statement
import os
import dateutil.tz
+import subprocess
_cache_tz = None
def _get_localzone():
- tzname = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip()
+ 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[link.rfind('/', 0, link.rfind('/')) + 1:]
+ tzname = link.split('zoneinfo/')[-1]
+ tzname = tzname.strip()
try:
+ # test the name
+ assert tzname
dateutil.tz.gettz(tzname)
return tzname
except:
diff --git a/nikola/packages/tzlocal/unix.py b/nikola/packages/tzlocal/unix.py
index 8e913b8..4cd3752 100644
--- a/nikola/packages/tzlocal/unix.py
+++ b/nikola/packages/tzlocal/unix.py
@@ -30,7 +30,7 @@ def _get_localzone():
try:
# link will be something like /usr/share/zoneinfo/America/Los_Angeles.
link = os.readlink('/etc/localtime')
- tz = link[link.rfind('/', 0, link.rfind('/')) + 1:]
+ tz = link.split('zoneinfo/')[-1]
if tz:
dateutil.tz.gettz(tz)
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
index 5eab806..e9af6b5 100644
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,6 +27,12 @@
from __future__ import absolute_import
import sys
import os
+import re
+
+from yapsy.IPlugin import IPlugin
+from doit.cmd_base import Command as DoitCommand
+
+from .utils import LOGGER, first_line
__all__ = [
'Command',
@@ -37,14 +43,11 @@ __all__ = [
'Task',
'TaskMultiplier',
'TemplateSystem',
- 'SignalHandler'
+ 'SignalHandler',
+ 'ConfigPlugin',
+ 'PostScanner',
]
-from yapsy.IPlugin import IPlugin
-from doit.cmd_base import Command as DoitCommand
-
-from .utils import LOGGER, first_line
-
class BasePlugin(IPlugin):
"""Base plugin class."""
@@ -75,6 +78,18 @@ class BasePlugin(IPlugin):
# so let’s just ignore it and be done with it.
pass
+ def inject_dependency(self, target, dependency):
+ """Add 'dependency' to the target task's task_deps"""
+ self.site.injected_deps[target].append(dependency)
+
+
+class PostScanner(BasePlugin):
+ """The scan method of these plugins is called by Nikola.scan_posts."""
+
+ def scan(self):
+ """Creates a list of posts from some source. Returns a list of Post objects."""
+ raise NotImplementedError()
+
class Command(BasePlugin, DoitCommand):
"""These plugins are exposed via the command line.
@@ -93,13 +108,21 @@ class Command(BasePlugin, DoitCommand):
BasePlugin.__init__(self, *args, **kwargs)
DoitCommand.__init__(self)
- def execute(self, options={}, args=[]):
+ def __call__(self, config=None, **kwargs):
+ self._doitargs = kwargs
+ DoitCommand.__init__(self, config, **kwargs)
+ return self
+
+ def execute(self, options=None, args=None):
"""Check if the command can run in the current environment,
fail if needed, or call _execute."""
+ options = options or {}
+ args = args or []
+
if self.needs_config and not self.site.configured:
LOGGER.error("This command needs to run inside an existing Nikola site.")
return False
- self._execute(options, args)
+ return self._execute(options, args)
def _execute(self, options, args):
"""Do whatever this command does.
@@ -117,7 +140,7 @@ def help(self):
text.append('')
text.append("Options:")
- for opt in self.options:
+ for opt in self.cmdparser.options:
text.extend(opt.help_doc())
if self.doc_description is not None:
@@ -208,20 +231,28 @@ class TaskMultiplier(BasePlugin):
class PageCompiler(BasePlugin):
"""Plugins that compile text files into HTML."""
- name = "dummy compiler"
+ name = "dummy_compiler"
+ friendly_name = ''
demote_headers = False
supports_onefile = True
- default_metadata = {}
-
default_metadata = {
'title': '',
'slug': '',
'date': '',
'tags': '',
+ 'category': '',
'link': '',
'description': '',
'type': 'text',
}
+ config_dependencies = []
+
+ def register_extra_dependencies(self, post):
+ """Add additional dependencies to the post object.
+
+ Current main use is the ReST page compiler, which puts extra
+ dependencies into a .deb file."""
+ pass
def compile_html(self, source, dest, is_two_file=False):
"""Compile the source, save it on dest."""
@@ -235,6 +266,23 @@ class PageCompiler(BasePlugin):
"""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):
+ """
+ Read the metadata from a post, and return a metadata dict
+ """
+ return {}
+
+ def split_metadata(self, data):
+ """Split data from metadata in the raw post content.
+
+ This splits in the first empty line that is NOT at the beginning
+ of the document."""
+ 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]
+
class RestExtension(BasePlugin):
name = "dummy_rest_extension"
@@ -248,6 +296,11 @@ class SignalHandler(BasePlugin):
name = "dummy_signal_handler"
+class ConfigPlugin(BasePlugin):
+ """A plugin that can edit config (or modify the site) on-the-fly."""
+ name = "dummy_config_plugin"
+
+
class Importer(Command):
"""Basic structure for importing data into Nikola.
diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py
index 764968a..f8a3a3c 100644
--- a/nikola/plugins/basic_import.py
+++ b/nikola/plugins/basic_import.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,6 +29,7 @@ import io
import csv
import datetime
import os
+import sys
from pkg_resources import resource_filename
try:
@@ -114,32 +115,34 @@ class ImportMixin(object):
return content
@classmethod
- def write_content(cls, filename, content):
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
+ def write_content(cls, filename, content, rewrite_html=True):
+ if rewrite_html:
+ doc = html.document_fromstring(content)
+ doc.rewrite_links(replacer)
+ content = html.tostring(doc, encoding='utf8')
+ else:
+ content = content.encode('utf-8')
utils.makedirs(os.path.dirname(filename))
with open(filename, "wb+") as fd:
- fd.write(html.tostring(doc, encoding='utf8'))
+ fd.write(content)
@staticmethod
- def write_metadata(filename, title, slug, post_date, description, tags):
+ def write_metadata(filename, title, slug, post_date, description, tags, **kwargs):
if not description:
description = ""
utils.makedirs(os.path.dirname(filename))
with io.open(filename, "w+", encoding="utf8") as fd:
- fd.write('{0}\n'.format(title))
- fd.write('{0}\n'.format(slug))
- fd.write('{0}\n'.format(post_date))
- fd.write('{0}\n'.format(','.join(tags)))
- fd.write('\n')
- fd.write('{0}\n'.format(description))
+ data = {'title': title, 'slug': slug, 'date': post_date, 'tags': ','.join(tags), 'description': description}
+ data.update(kwargs)
+ fd.write(utils.write_metadata(data))
@staticmethod
def write_urlmap_csv(output_file, url_map):
utils.makedirs(os.path.dirname(output_file))
- with io.open(output_file, 'w+', encoding='utf8') as fd:
+ fmode = 'wb+' if sys.version_info[0] == 2 else 'w+'
+ with io.open(output_file, fmode) as fd:
csv_writer = csv.writer(fd)
for item in url_map.items():
csv_writer.writerow(item)
diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/command/__init__.py
+++ b/nikola/plugins/command/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 87939b2..a1c6820 100644
--- a/nikola/plugins/command/auto.plugin
+++ b/nikola/plugins/command/auto.plugin
@@ -4,6 +4,6 @@ Module = auto
[Documentation]
Author = Roberto Alsina
-Version = 0.2
+Version = 2.1.0
Website = http://getnikola.com
Description = Automatically detect site changes, rebuild and optionally refresh a browser.
diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py
deleted file mode 100644
index 7f3f66f..0000000
--- a/nikola/plugins/command/auto.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2014 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function, unicode_literals
-
-import os
-import subprocess
-
-from nikola.plugin_categories import Command
-from nikola.utils import req_missing
-
-
-class CommandAuto(Command):
- """Start debugging console."""
- name = "auto"
- doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser"
- cmd_options = [
- {
- 'name': 'browser',
- 'short': 'b',
- 'type': bool,
- 'help': 'Start a web browser.',
- 'default': False,
- },
- {
- 'name': 'port',
- 'short': 'p',
- 'long': 'port',
- 'default': 8000,
- 'type': int,
- 'help': 'Port nummber (default: 8000)',
- },
- ]
-
- def _execute(self, options, args):
- """Start the watcher."""
- try:
- from livereload import Server
- except ImportError:
- req_missing(['livereload'], 'use the "auto" command')
- return
-
- # Run an initial build so we are up-to-date
- subprocess.call(("nikola", "build"))
-
- port = options and options.get('port')
-
- server = Server()
- server.watch('conf.py', 'nikola build')
- server.watch('themes/', 'nikola build')
- server.watch('templates/', 'nikola build')
- server.watch(self.site.config['GALLERY_PATH'], 'nikola build')
- for item in self.site.config['post_pages']:
- server.watch(os.path.dirname(item[0]), 'nikola build')
- for item in self.site.config['FILES_FOLDERS']:
- server.watch(item, 'nikola build')
-
- out_folder = self.site.config['OUTPUT_FOLDER']
- if options and options.get('browser'):
- browser = True
- else:
- browser = False
-
- server.serve(port, None, out_folder, True, browser)
diff --git a/nikola/plugins/command/auto/__init__.py b/nikola/plugins/command/auto/__init__.py
new file mode 100644
index 0000000..c25ef8a
--- /dev/null
+++ b/nikola/plugins/command/auto/__init__.py
@@ -0,0 +1,366 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2015 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+
+import json
+import mimetypes
+import os
+import re
+import subprocess
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse # NOQA
+import webbrowser
+from wsgiref.simple_server import make_server
+import wsgiref.util
+
+from blinker import signal
+try:
+ from ws4py.websocket import WebSocket
+ from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
+ from ws4py.server.wsgiutils import WebSocketWSGIApplication
+ from ws4py.messaging import TextMessage
+except ImportError:
+ WebSocket = object
+try:
+ import watchdog
+ from watchdog.observers import Observer
+ from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler
+except ImportError:
+ watchdog = None
+ FileSystemEventHandler = object
+ PatternMatchingEventHandler = object
+
+
+from nikola.plugin_categories import Command
+from nikola.utils import req_missing, get_logger, get_theme_path
+LRJS_PATH = os.path.join(os.path.dirname(__file__), 'livereload.js')
+error_signal = signal('error')
+refresh_signal = signal('refresh')
+
+ERROR_N = '''<html>
+<head>
+</head>
+<boody>
+ERROR {}
+</body>
+</html>
+'''
+
+
+class CommandAuto(Command):
+ """Start debugging console."""
+ name = "auto"
+ logger = None
+ doc_purpose = "builds and serves a site; automatically detects site changes, rebuilds, and optionally refreshes a browser"
+ cmd_options = [
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ {
+ 'name': 'address',
+ 'short': 'a',
+ 'long': 'address',
+ 'type': str,
+ 'default': '127.0.0.1',
+ 'help': 'Address to bind (default: 127.0.0.1 – localhost)',
+ },
+ {
+ 'name': 'browser',
+ 'short': 'b',
+ 'long': 'browser',
+ 'type': bool,
+ 'help': 'Start a web browser.',
+ 'default': False,
+ },
+ {
+ 'name': 'ipv6',
+ 'short': '6',
+ 'long': 'ipv6',
+ 'default': False,
+ 'type': bool,
+ 'help': 'Use IPv6',
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Start the watcher."""
+
+ self.logger = get_logger('auto', self.site.loghandlers)
+ 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:
+ req_missing(['watchdog'], 'use the "auto" command')
+
+ self.cmd_arguments = ['nikola', 'build']
+ if self.site.configuration_filename != 'conf.py':
+ self.cmd_arguments = ['--conf=' + self.site.configuration_filename] + self.cmd_arguments
+
+ # Run an initial build so we are up-to-date
+ subprocess.call(self.cmd_arguments)
+
+ port = options and options.get('port')
+ self.snippet = '''<script>document.write('<script src="http://'
+ + (location.host || 'localhost').split(':')[0]
+ + ':{0}/livereload.js?snipver=1"></'
+ + 'script>')</script>
+ </head>'''.format(port)
+
+ # Do not duplicate entries -- otherwise, multiple rebuilds are triggered
+ watched = set([
+ 'templates/',
+ ] + [get_theme_path(name) for name in self.site.THEMES])
+ for item in self.site.config['post_pages']:
+ watched.add(os.path.dirname(item[0]))
+ for item in self.site.config['FILES_FOLDERS']:
+ watched.add(item)
+ for item in self.site.config['GALLERY_FOLDERS']:
+ watched.add(item)
+ for item in self.site.config['LISTINGS_FOLDERS']:
+ watched.add(item)
+
+ out_folder = self.site.config['OUTPUT_FOLDER']
+ if options and options.get('browser'):
+ browser = True
+ else:
+ browser = False
+
+ if options['ipv6']:
+ dhost = '::'
+ else:
+ dhost = None
+
+ host = options['address'].strip('[').strip(']') or dhost
+
+ # Instantiate global observer
+ observer = Observer()
+ # Watch output folders and trigger reloads
+ observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True)
+
+ # Watch input folders and trigger rebuilds
+ for p in watched:
+ if os.path.exists(p):
+ observer.schedule(OurWatchHandler(self.do_rebuild), 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)
+
+ observer.start()
+
+ parent = self
+
+ class Mixed(WebSocketWSGIApplication):
+ """A class that supports WS and HTTP protocols in the same port."""
+ def __call__(self, environ, start_response):
+ if environ.get('HTTP_UPGRADE') is None:
+ return parent.serve_static(environ, start_response)
+ return super(Mixed, self).__call__(environ, start_response)
+
+ ws = make_server(
+ host, port, server_class=WSGIServer,
+ handler_class=WebSocketWSGIRequestHandler,
+ app=Mixed(handler_cls=LRSocket)
+ )
+ ws.initialize_websockets_manager()
+ self.logger.info("Serving HTTP on {0} port {1}...".format(host, port))
+ if browser:
+ if options['ipv6'] or '::' in host:
+ server_url = "http://[{0}]:{1}/".format(host, port)
+ else:
+ server_url = "http://{0}:{1}/".format(host, port)
+
+ self.logger.info("Opening {0} in the default web browser...".format(server_url))
+ # Yes, this is racy
+ webbrowser.open('http://{0}:{1}'.format(host, port))
+
+ try:
+ ws.serve_forever()
+ except KeyboardInterrupt:
+ self.logger.info("Server is shutting down.")
+ observer.stop()
+ observer.join()
+
+ def do_rebuild(self, event):
+ self.logger.info('REBUILDING SITE (from {0})'.format(event.src_path))
+ p = subprocess.Popen(self.cmd_arguments, stderr=subprocess.PIPE)
+ if p.wait() != 0:
+ error = p.stderr.read()
+ self.logger.error(error)
+ error_signal.send(error=error)
+ else:
+ error = p.stderr.read()
+ print(error)
+
+ def do_refresh(self, event):
+ self.logger.info('REFRESHING: {0}'.format(event.src_path))
+ p = os.path.relpath(event.src_path, os.path.abspath(self.site.config['OUTPUT_FOLDER']))
+ refresh_signal.send(path=p)
+
+ def serve_static(self, environ, start_response):
+ """Trivial static file server."""
+ uri = wsgiref.util.request_uri(environ)
+ p_uri = urlparse(uri)
+ f_path = os.path.join(self.site.config['OUTPUT_FOLDER'], *p_uri.path.split('/'))
+ mimetype = mimetypes.guess_type(uri)[0] or 'text/html'
+
+ if os.path.isdir(f_path):
+ f_path = os.path.join(f_path, self.site.config['INDEX_FILE'])
+
+ if p_uri.path == '/robots.txt':
+ start_response('200 OK', [('Content-type', 'text/plain')])
+ return ['User-Agent: *\nDisallow: /\n']
+ elif os.path.isfile(f_path):
+ with open(f_path, 'rb') as fd:
+ start_response('200 OK', [('Content-type', mimetype)])
+ return [self.inject_js(mimetype, fd.read())]
+ elif p_uri.path == '/livereload.js':
+ with open(LRJS_PATH, 'rb') as fd:
+ start_response('200 OK', [('Content-type', mimetype)])
+ return [self.inject_js(mimetype, fd.read())]
+ start_response('404 ERR', [])
+ return [self.inject_js('text/html', ERROR_N.format(404).format(uri))]
+
+ def inject_js(self, mimetype, data):
+ """Inject livereload.js in HTML files."""
+ if mimetype == 'text/html':
+ data = re.sub('</head>', self.snippet, data.decode('utf8'), 1, re.IGNORECASE)
+ data = data.encode('utf8')
+ return data
+
+
+pending = []
+
+
+class LRSocket(WebSocket):
+ """Speak Livereload protocol."""
+
+ def __init__(self, *a, **kw):
+ refresh_signal.connect(self.notify)
+ error_signal.connect(self.send_error)
+ super(LRSocket, self).__init__(*a, **kw)
+
+ def received_message(self, 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)
+ else:
+ self.send(response, response.is_binary)
+
+ 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)
+ else:
+ self.send(response, response.is_binary)
+
+
+class OurWatchHandler(FileSystemEventHandler):
+
+ """A Nikola-specific handler for Watchdog."""
+
+ def __init__(self, function):
+ """Initialize the handler."""
+ self.function = function
+ super(OurWatchHandler, self).__init__()
+
+ def on_any_event(self, event):
+ """Call the provided function on any event."""
+ self.function(event)
+
+
+class ConfigWatchHandler(FileSystemEventHandler):
+
+ """A Nikola-specific handler for Watchdog that handles the config file (as a workaround)."""
+
+ def __init__(self, configuration_filename, function):
+ """Initialize the handler."""
+ self.configuration_filename = configuration_filename
+ self.function = function
+
+ def on_any_event(self, event):
+ """Call the provided function on any event."""
+ if event._src_path == self.configuration_filename:
+ self.function(event)
diff --git a/nikola/plugins/command/auto/livereload.js b/nikola/plugins/command/auto/livereload.js
new file mode 120000
index 0000000..b4cafb3
--- /dev/null
+++ b/nikola/plugins/command/auto/livereload.js
@@ -0,0 +1 @@
+../../../../bower_components/livereload-js/dist/livereload.js \ No newline at end of file
diff --git a/nikola/plugins/command/bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin
index 7091310..b428da3 100644
--- a/nikola/plugins/command/bootswatch_theme.plugin
+++ b/nikola/plugins/command/bootswatch_theme.plugin
@@ -4,7 +4,7 @@ Module = bootswatch_theme
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Given a swatch name and a parent theme, creates a custom theme.
diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py
index e65413b..e19c937 100644
--- a/nikola/plugins/command/bootswatch_theme.py
+++ b/nikola/plugins/command/bootswatch_theme.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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,7 @@
from __future__ import print_function
import os
-
-try:
- import requests
-except ImportError:
- requests = None # NOQA
+import requests
from nikola.plugin_categories import Command
from nikola import utils
@@ -57,7 +53,7 @@ class CommandBootswatchTheme(Command):
{
'name': 'swatch',
'short': 's',
- 'default': 'slate',
+ 'default': '',
'type': str,
'help': 'Name of the swatch from bootswatch.com.'
},
@@ -72,19 +68,19 @@ class CommandBootswatchTheme(Command):
def _execute(self, options, args):
"""Given a swatch name and a parent theme, creates a custom theme."""
- if requests is None:
- utils.req_missing(['requests'], 'install Bootswatch themes')
-
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)
- if 'bootstrap3' not in themes or 'bootstrap3-jinja' not in themes:
+ if 'bootstrap3' not in themes and 'bootstrap3-jinja' not in themes:
version = '2'
- elif 'bootstrap' not in themes or 'bootstrap-jinja' not in themes:
+ elif 'bootstrap' not in themes and 'bootstrap-jinja' not in themes:
LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap')
elif 'bootstrap3-gradients' in themes or 'bootstrap3-gradients-jinja' in themes:
LOGGER.warn('"bootswatch_theme" doesn\'t work well with the bootstrap3-gradients family')
diff --git a/nikola/plugins/command/check.plugin b/nikola/plugins/command/check.plugin
index 8ceda5f..dd0980e 100644
--- a/nikola/plugins/command/check.plugin
+++ b/nikola/plugins/command/check.plugin
@@ -4,7 +4,7 @@ Module = check
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Check the generated site
diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py
index bd254f4..a9bc44a 100644
--- a/nikola/plugins/command/check.py
+++ b/nikola/plugins/command/check.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,6 +25,7 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
+from collections import defaultdict
import os
import re
import sys
@@ -34,21 +35,36 @@ try:
except ImportError:
from urllib.parse import unquote, urlparse, urljoin, urldefrag # NOQA
+from doit.loader import generate_tasks
import lxml.html
+import requests
from nikola.plugin_categories import Command
from nikola.utils import get_logger
+def _call_nikola_list(site):
+ files = []
+ deps = defaultdict(list)
+ for task in generate_tasks('render_site', site.gen_tasks('render_site', "Task", '')):
+ files.extend(task.targets)
+ for target in task.targets:
+ deps[target].extend(task.file_dep)
+ for task in generate_tasks('post_render', site.gen_tasks('render_site', "LateTask", '')):
+ files.extend(task.targets)
+ for target in task.targets:
+ deps[target].extend(task.file_dep)
+ return files, deps
+
+
def real_scan_files(site):
task_fnames = set([])
real_fnames = set([])
output_folder = site.config['OUTPUT_FOLDER']
# First check that all targets are generated in the right places
- for task in os.popen('nikola list --all', 'r').readlines():
- task = task.strip()
- if output_folder in task and ':' in task:
- fname = task.split(':', 1)[-1]
+ for fname in _call_nikola_list(site)[0]:
+ fname = fname.strip()
+ if fname.startswith(output_folder):
task_fnames.add(fname)
# And now check that there are no non-target files
for root, dirs, files in os.walk(output_folder, followlinks=True):
@@ -68,7 +84,7 @@ def fs_relpath_from_url_path(url_path):
url_path = unquote(url_path)
# in windows relative paths don't begin with os.sep
if sys.platform == 'win32' and len(url_path):
- url_path = url_path[1:].replace('/', '\\')
+ url_path = url_path.replace('/', '\\')
return url_path
@@ -78,7 +94,7 @@ class CommandCheck(Command):
name = "check"
logger = None
- doc_usage = "-l [--find-sources] | -f"
+ doc_usage = "[-v] (-l [--find-sources] [-r] | -f [--clean-files])"
doc_purpose = "check links and files in the generated site"
cmd_options = [
{
@@ -119,11 +135,18 @@ class CommandCheck(Command):
'default': False,
'help': 'Be more verbose.',
},
+ {
+ 'name': 'remote',
+ 'long': 'remote',
+ 'short': 'r',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Check that remote links work.',
+ },
]
def _execute(self, options, args):
"""Check the generated site."""
-
self.logger = get_logger('check', self.site.loghandlers)
if not options['links'] and not options['files'] and not options['clean']:
@@ -134,59 +157,103 @@ class CommandCheck(Command):
else:
self.logger.level = 4
if options['links']:
- failure = self.scan_links(options['find_sources'])
+ failure = self.scan_links(options['find_sources'], options['remote'])
if options['files']:
failure = self.scan_files()
if options['clean']:
failure = self.clean_files()
if failure:
- sys.exit(1)
+ return 1
existing_targets = set([])
+ checked_remote_targets = {}
- def analyze(self, task, find_sources=False):
+ def analyze(self, fname, find_sources=False, check_remote=False):
rv = False
self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']]
base_url = urlparse(self.site.config['BASE_URL'])
self.existing_targets.add(self.site.config['SITE_URL'])
self.existing_targets.add(self.site.config['BASE_URL'])
url_type = self.site.config['URL_TYPE']
- if url_type == 'absolute':
- url_netloc_to_root = urlparse(self.site.config['SITE_URL']).path
+
+ deps = {}
+ if find_sources:
+ deps = _call_nikola_list(self.site)[1]
+
+ if url_type in ('absolute', 'full_path'):
+ url_netloc_to_root = urlparse(self.site.config['BASE_URL']).path
try:
- filename = task.split(":")[-1]
- d = lxml.html.fromstring(open(filename).read())
+ filename = fname
+
+ if filename.startswith(self.site.config['CACHE_FOLDER']):
+ # 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))
+ return False
+
+ if not os.path.exists(fname):
+ # Quietly ignore files that don’t exist; use `nikola check -f` instead (Issue #1831)
+ return False
+
+ d = lxml.html.fromstring(open(filename, 'rb').read())
for l in d.iterlinks():
- target = l[0].attrib[l[1]]
+ target = l[2]
if target == "#":
continue
target, _ = urldefrag(target)
parsed = urlparse(target)
- # Absolute links when using only paths, skip.
- if (parsed.scheme or target.startswith('//')) and url_type in ('rel_path', 'full_path'):
- continue
+ # 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))
# Absolute links to other domains, skip
- if (parsed.scheme or target.startswith('//')) and parsed.netloc != base_url.netloc:
+ # Absolute links when using only paths, skip.
+ if ((parsed.scheme or target.startswith('//')) and parsed.netloc != base_url.netloc) or \
+ ((parsed.scheme or target.startswith('//')) and url_type in ('rel_path', 'full_path')):
+ if not check_remote or parsed.scheme not in ["http", "https"]:
+ continue
+ if parsed.netloc == base_url.netloc: # absolute URL to self.site
+ continue
+ if target in self.checked_remote_targets: # already checked this exact target
+ if self.checked_remote_targets[target] > 399:
+ self.logger.warn("Broken link in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target]))
+ continue
+ # Check the remote link works
+ req_headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0 (Nikola)'} # I’m a real boy!
+ resp = requests.head(target, headers=req_headers)
+ self.checked_remote_targets[target] = resp.status_code
+ if resp.status_code > 399: # Error
+ self.logger.warn("Broken link in {0}: {1} [Error {2}]".format(filename, target, resp.status_code))
+ continue
+ elif resp.status_code <= 399: # The address leads *somewhere* that is not an error
+ self.logger.debug("Successfully checked remote link in {0}: {1} [HTTP: {2}]".format(filename, target, resp.status_code))
+ continue
+ self.logger.warn("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target))
continue
if url_type == 'rel_path':
- target_filename = os.path.abspath(
- os.path.join(os.path.dirname(filename), unquote(target)))
+ if target.startswith('/'):
+ target_filename = os.path.abspath(
+ os.path.join(self.site.config['OUTPUT_FOLDER'], unquote(target.lstrip('/'))))
+ else: # Relative path
+ target_filename = os.path.abspath(
+ os.path.join(os.path.dirname(filename), unquote(target)))
elif url_type in ('full_path', 'absolute'):
if url_type == 'absolute':
# convert to 'full_path' case, ie url relative to root
- url_rel_path = target.path[len(url_netloc_to_root):]
+ url_rel_path = parsed.path[len(url_netloc_to_root):]
else:
- url_rel_path = target.path
+ # convert to relative to base path
+ url_rel_path = target[len(url_netloc_to_root):]
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 any(re.match(x, target_filename) for x in self.whitelist):
+ if any(re.search(x, target_filename) for x in self.whitelist):
continue
elif target_filename not in self.existing_targets:
if os.path.exists(target_filename):
@@ -197,25 +264,22 @@ class CommandCheck(Command):
self.logger.warn("Broken link in {0}: {1}".format(filename, target))
if find_sources:
self.logger.warn("Possible sources:")
- self.logger.warn(os.popen('nikola list --deps ' + task, 'r').read())
+ self.logger.warn("\n".join(deps[filename]))
self.logger.warn("===============================\n")
except Exception as exc:
self.logger.error("Error with: {0} {1}".format(filename, exc))
return rv
- def scan_links(self, find_sources=False):
+ def scan_links(self, find_sources=False, check_remote=False):
self.logger.info("Checking Links:")
self.logger.info("===============\n")
self.logger.notice("{0} mode".format(self.site.config['URL_TYPE']))
failure = False
- for task in os.popen('nikola list --all', 'r').readlines():
- task = task.strip()
- if task.split(':')[0] in (
- 'render_tags', 'render_archive',
- 'render_galleries', 'render_indexes',
- 'render_pages'
- 'render_site') and '.html' in task:
- if self.analyze(task, find_sources):
+ # Maybe we should just examine all HTML files
+ output_folder = self.site.config['OUTPUT_FOLDER']
+ for fname in _call_nikola_list(self.site)[0]:
+ if fname.startswith(output_folder) and '.html' == fname[-5:]:
+ if self.analyze(fname, find_sources, check_remote):
failure = True
if not failure:
self.logger.info("All links checked.")
diff --git a/nikola/plugins/command/console.plugin b/nikola/plugins/command/console.plugin
index 2eeedae..3aef2e7 100644
--- a/nikola/plugins/command/console.plugin
+++ b/nikola/plugins/command/console.plugin
@@ -4,6 +4,6 @@ Module = console
[Documentation]
Author = Chris Warrick, Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Start a debugging python console
diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py
index 9dfc975..b8e7825 100644
--- a/nikola/plugins/command/console.py
+++ b/nikola/plugins/command/console.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Chris Warrick, Roberto Alsina and others.
+# Copyright © 2012-2015 Chris Warrick, Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,7 +30,7 @@ import os
from nikola import __version__
from nikola.plugin_categories import Command
-from nikola.utils import get_logger, STDERR_HANDLER, req_missing
+from nikola.utils import get_logger, STDERR_HANDLER, req_missing, Commands
LOGGER = get_logger('console', STDERR_HANDLER)
@@ -122,6 +122,8 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f
self.site.scan_posts()
# Create nice object with all commands:
+ self.site.commands = Commands(self.site.doit, self.config, self._doitargs)
+
self.context = {
'conf': self.site.config,
'site': self.site,
diff --git a/nikola/plugins/command/deploy.plugin b/nikola/plugins/command/deploy.plugin
index 10cc796..14fd53f 100644
--- a/nikola/plugins/command/deploy.plugin
+++ b/nikola/plugins/command/deploy.plugin
@@ -4,6 +4,6 @@ Module = deploy
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Deploy the site
diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py
index fde43fa..2c44e87 100644
--- a/nikola/plugins/command/deploy.py
+++ b/nikola/plugins/command/deploy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,23 +29,22 @@ import io
from datetime import datetime
from dateutil.tz import gettz
import os
-import sys
import subprocess
import time
from blinker import signal
from nikola.plugin_categories import Command
-from nikola.utils import get_logger, remove_file, unicode_str
+from nikola.utils import get_logger, remove_file, unicode_str, makedirs
class CommandDeploy(Command):
"""Deploy site."""
name = "deploy"
- doc_usage = ""
+ 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):
@@ -74,14 +73,29 @@ class CommandDeploy(Command):
remove_file(os.path.join(out_dir, post.source_path))
undeployed_posts.append(post)
- for command in self.site.config['DEPLOY_COMMANDS']:
- self.logger.info("==> {0}".format(command))
+ if args:
+ presets = args
+ else:
+ presets = ['default']
+
+ # test for preset existence
+ for preset in presets:
try:
- subprocess.check_call(command, shell=True)
- except subprocess.CalledProcessError as e:
- self.logger.error('Failed deployment — command {0} '
- 'returned {1}'.format(e.cmd, e.returncode))
- sys.exit(e.returncode)
+ self.site.config['DEPLOY_COMMANDS'][preset]
+ except:
+ self.logger.error('No such preset: {0}'.format(preset))
+ return 255
+
+ for preset in presets:
+ self.logger.info("=> preset '{0}'".format(preset))
+ for command in self.site.config['DEPLOY_COMMANDS'][preset]:
+ self.logger.info("==> {0}".format(command))
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ self.logger.error('Failed deployment — command {0} '
+ 'returned {1}'.format(e.cmd, e.returncode))
+ return e.returncode
self.logger.info("Successful deployment")
try:
@@ -96,6 +110,7 @@ class CommandDeploy(Command):
new_deploy = datetime.utcnow()
self._emit_deploy_event(last_deploy, new_deploy, clean, undeployed_posts)
+ makedirs(self.site.config['CACHE_FOLDER'])
# Store timestamp of successful deployment
with io.open(timestamp_path, 'w+', encoding='utf8') as outf:
outf.write(unicode_str(new_deploy.isoformat()))
diff --git a/nikola/plugins/command/github_deploy.plugin b/nikola/plugins/command/github_deploy.plugin
index 4cbc422..74e7902 100644
--- a/nikola/plugins/command/github_deploy.plugin
+++ b/nikola/plugins/command/github_deploy.plugin
@@ -4,6 +4,6 @@ Module = github_deploy
[Documentation]
Author = Puneeth Chaganti
-Version = 0.1
+Version = 1,0
Website = http://getnikola.com
Description = Deploy the site to GitHub pages.
diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py
index 13da48c..888a4f9 100644
--- a/nikola/plugins/command/github_deploy.py
+++ b/nikola/plugins/command/github_deploy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2014 Puneeth Chaganti and others.
+# Copyright © 2014-2015 Puneeth Chaganti and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,15 +25,15 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
+from datetime import datetime
+import io
import os
-import shutil
import subprocess
-import sys
from textwrap import dedent
from nikola.plugin_categories import Command
from nikola.plugins.command.check import real_scan_files
-from nikola.utils import ask_yesno, get_logger
+from nikola.utils import get_logger, req_missing, makedirs, unicode_str
from nikola.__main__ import main
from nikola import __version__
@@ -43,79 +43,53 @@ def uni_check_output(*args, **kwargs):
return o.decode('utf-8')
+def check_ghp_import_installed():
+ try:
+ subprocess.check_output(['ghp-import', '-h'])
+ except OSError:
+ # req_missing defaults to `python=True` — and it’s meant to be like this.
+ # `ghp-import` is installed via pip, but the only way to use it is by executing the script it installs.
+ req_missing(['ghp-import'], 'deploy the site to GitHub Pages')
+
+
class CommandGitHubDeploy(Command):
- """ Deploy site to GitHub pages. """
+ """ Deploy site to GitHub Pages. """
name = 'github_deploy'
doc_usage = ''
- doc_purpose = 'deploy the site to GitHub pages'
+ doc_purpose = 'deploy the site to GitHub Pages'
doc_description = dedent(
"""\
- This command can be used to deploy your site to GitHub pages.
- It performs the following actions:
+ This command can be used to deploy your site to GitHub Pages.
- 1. Ensure that your site is a git repository, and git is on the PATH.
- 2. Ensure that the output directory is not committed on the
- source branch.
- 3. Check for changes, and prompt the user to continue, if required.
- 4. Build the site
- 5. Clean any files that are "unknown" to Nikola.
- 6. Create a deploy branch, if one doesn't exist.
- 7. Commit the output to this branch. (NOTE: Any untracked source
- files, may get committed at this stage, on the wrong branch!)
- 8. Push and deploy!
+ It uses ghp-import to do this task.
- NOTE: This command needs your site to be a git repository, with a
- master branch (or a different branch, configured using
- GITHUB_SOURCE_BRANCH if you are pushing to user.github
- .io/organization.github.io pages) containing the sources of your
- site. You also, obviously, need to have `git` on your PATH,
- and should be able to push to the repository specified as the remote
- (origin, by default).
"""
)
logger = None
- _deploy_branch = ''
- _source_branch = ''
- _remote_name = ''
-
def _execute(self, command, args):
self.logger = get_logger(
CommandGitHubDeploy.name, self.site.loghandlers
)
- self._source_branch = self.site.config.get(
- 'GITHUB_SOURCE_BRANCH', 'master'
- )
- self._deploy_branch = self.site.config.get(
- 'GITHUB_DEPLOY_BRANCH', 'gh-pages'
- )
- self._remote_name = self.site.config.get(
- 'GITHUB_REMOTE_NAME', 'origin'
- )
-
- self._ensure_git_repo()
-
- self._exit_if_output_committed()
- if not self._prompt_continue():
- return
+ # Check if ghp-import is installed
+ check_ghp_import_installed()
+ # Build before deploying
build = main(['build'])
if build != 0:
self.logger.error('Build failed, not deploying to GitHub')
- sys.exit(build)
+ return build
+ # Clean non-target files
only_on_output, _ = real_scan_files(self.site)
for f in only_on_output:
os.unlink(f)
- self._checkout_deploy_branch()
-
- self._copy_output()
-
+ # Commit and push
self._commit_and_push()
return
@@ -123,150 +97,34 @@ class CommandGitHubDeploy(Command):
def _commit_and_push(self):
""" Commit all the files and push. """
- deploy = self._deploy_branch
- source = self._source_branch
- remote = self._remote_name
-
+ source = self.site.config['GITHUB_SOURCE_BRANCH']
+ deploy = self.site.config['GITHUB_DEPLOY_BRANCH']
+ remote = self.site.config['GITHUB_REMOTE_NAME']
source_commit = uni_check_output(['git', 'rev-parse', source])
commit_message = (
'Nikola auto commit.\n\n'
'Source commit: %s'
'Nikola version: %s' % (source_commit, __version__)
)
-
- commands = [
- ['git', 'pull', remote, '%s:%s' % (deploy, deploy)],
- ['git', 'add', '-A'],
- ['git', 'commit', '-m', commit_message],
- ['git', 'push', remote, '%s:%s' % (deploy, deploy)],
- ['git', 'checkout', source],
- ]
-
- for command in commands:
- self.logger.info("==> {0}".format(command))
- try:
- subprocess.check_call(command)
- except subprocess.CalledProcessError as e:
- self.logger.error(
- 'Failed GitHub deployment — command {0} '
- 'returned {1}'.format(e.cmd, e.returncode)
- )
- sys.exit(e.returncode)
-
- def _copy_output(self):
- """ Copy all output to the top level directory. """
output_folder = self.site.config['OUTPUT_FOLDER']
- for each in os.listdir(output_folder):
- if os.path.exists(each):
- if os.path.isdir(each):
- shutil.rmtree(each)
-
- else:
- os.unlink(each)
-
- shutil.move(os.path.join(output_folder, each), '.')
-
- def _checkout_deploy_branch(self):
- """ Check out the deploy branch
-
- Creates an orphan branch if not present.
-
- """
- deploy = self._deploy_branch
+ command = ['ghp-import', '-n', '-m', commit_message, '-p', '-r', remote, '-b', deploy, output_folder]
+ self.logger.info("==> {0}".format(command))
try:
- subprocess.check_call(
- [
- 'git', 'show-ref', '--verify', '--quiet',
- 'refs/heads/%s' % deploy
- ]
- )
- except subprocess.CalledProcessError:
- self._create_orphan_deploy_branch()
- else:
- subprocess.check_call(['git', 'checkout', deploy])
-
- def _create_orphan_deploy_branch(self):
- """ Create an orphan deploy branch """
-
- result = subprocess.check_call(
- ['git', 'checkout', '--orphan', self._deploy_branch]
- )
- if result != 0:
- self.logger.error('Failed to create a deploy branch')
- sys.exit(1)
-
- result = subprocess.check_call(['git', 'rm', '-rf', '.'])
- if result != 0:
- self.logger.error('Failed to create a deploy branch')
- sys.exit(1)
-
- with open('.gitignore', 'w') as f:
- f.write('%s\n' % self.site.config['OUTPUT_FOLDER'])
- f.write('%s\n' % self.site.config['CACHE_FOLDER'])
- f.write('*.pyc\n')
- f.write('*.db\n')
-
- subprocess.check_call(['git', 'add', '.gitignore'])
- subprocess.check_call(['git', 'commit', '-m', 'Add .gitignore'])
-
- def _ensure_git_repo(self):
- """ Ensure that the site is a git-repo.
-
- Also make sure that a remote with the specified name exists.
-
- """
-
- try:
- remotes = uni_check_output(['git', 'remote'])
+ subprocess.check_call(command)
except subprocess.CalledProcessError as e:
- self.logger.notice('github_deploy needs a git repository!')
- sys.exit(e.returncode)
- except OSError as e:
- import errno
- self.logger.error('Running git failed with {0}'.format(e))
- if e.errno == errno.ENOENT:
- self.logger.notice('Is git on the PATH?')
- sys.exit(1)
- else:
- if self._remote_name not in remotes:
- self.logger.error(
- 'Need a remote called "%s" configured' % self._remote_name
- )
- sys.exit(1)
-
- def _exit_if_output_committed(self):
- """ Exit if the output folder is committed on the source branch. """
-
- source = self._source_branch
- subprocess.check_call(['git', 'checkout', source])
-
- output_folder = self.site.config['OUTPUT_FOLDER']
- output_log = uni_check_output(
- ['git', 'ls-files', '--', output_folder]
- )
-
- if len(output_log.strip()) > 0:
self.logger.error(
- 'Output folder is committed on the source branch. '
- 'Cannot proceed until it is removed.'
+ 'Failed GitHub deployment — command {0} '
+ 'returned {1}'.format(e.cmd, e.returncode)
)
- sys.exit(1)
-
- def _prompt_continue(self):
- """ Show uncommitted changes, and ask if user wants to continue. """
+ return e.returncode
- changes = uni_check_output(['git', 'status', '--porcelain'])
- if changes.strip():
- changes = uni_check_output(['git', 'status']).strip()
- message = (
- "You have the following changes:\n%s\n\n"
- "Anything not committed, and unknown to Nikola may be lost, "
- "or committed onto the wrong branch. Do you wish to continue?"
- ) % changes
- proceed = ask_yesno(message, False)
- else:
- proceed = True
+ self.logger.info("Successful deployment")
- return proceed
+ # Store timestamp of successful deployment
+ timestamp_path = os.path.join(self.site.config["CACHE_FOLDER"], "lastdeploy")
+ new_deploy = datetime.utcnow()
+ makedirs(self.site.config["CACHE_FOLDER"])
+ with io.open(timestamp_path, "w+", encoding="utf8") as outf:
+ outf.write(unicode_str(new_deploy.isoformat()))
diff --git a/nikola/plugins/command/import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin
index fadc759..e072224 100644
--- a/nikola/plugins/command/import_wordpress.plugin
+++ b/nikola/plugins/command/import_wordpress.plugin
@@ -4,7 +4,7 @@ Module = import_wordpress
[Documentation]
Author = Roberto Alsina
-Version = 0.2
+Version = 1.0
Website = http://getnikola.com
Description = Import a wordpress site from a XML dump (requires markdown).
diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py
index 1af4083..674fc2a 100644
--- a/nikola/plugins/command/import_wordpress.py
+++ b/nikola/plugins/command/import_wordpress.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,6 +28,8 @@ from __future__ import unicode_literals, print_function
import os
import re
import sys
+import datetime
+import requests
from lxml import etree
try:
@@ -37,11 +39,6 @@ except ImportError:
from urllib.parse import urlparse, unquote # NOQA
try:
- import requests
-except ImportError:
- requests = None # NOQA
-
-try:
import phpserialize
except ImportError:
phpserialize = None # NOQA
@@ -87,6 +84,13 @@ class CommandImportWordpress(Command, ImportMixin):
'help': "Do not try to download files for the import",
},
{
+ 'name': 'download_auth',
+ 'long': 'download-auth',
+ 'default': None,
+ 'type': str,
+ 'help': "Specify username and password for HTTP authentication (separated by ':')",
+ },
+ {
'name': 'separate_qtranslate_content',
'long': 'qtranslate',
'default': False,
@@ -104,6 +108,7 @@ class CommandImportWordpress(Command, ImportMixin):
'help': "The pattern for translation files names",
},
]
+ all_tags = set([])
def _execute(self, options={}, args=[]):
"""Import a WordPress blog from an export file into a Nikola site."""
@@ -133,6 +138,14 @@ class CommandImportWordpress(Command, ImportMixin):
self.exclude_drafts = options.get('exclude_drafts', False)
self.no_downloads = options.get('no_downloads', False)
+ self.auth = None
+ if options.get('download_auth') is not None:
+ username_password = options.get('download_auth')
+ self.auth = tuple(username_password.split(':', 1))
+ if len(self.auth) < 2:
+ print("Please specify HTTP authentication credentials in the form username:password.")
+ return False
+
self.separate_qtranslate_content = options.get('separate_qtranslate_content')
self.translations_pattern = options.get('translations_pattern')
@@ -149,11 +162,7 @@ class CommandImportWordpress(Command, ImportMixin):
package=modulename)
)
- if requests is None and phpserialize is None:
- req_missing(['requests', 'phpserialize'], 'import WordPress dumps without --no-downloads')
- elif requests is None:
- req_missing(['requests'], 'import WordPress dumps without --no-downloads')
- elif phpserialize is None:
+ if phpserialize is None:
req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads')
channel = self.get_channel_from_file(self.wordpress_export_file)
@@ -172,6 +181,19 @@ class CommandImportWordpress(Command, ImportMixin):
self.extra_languages)
self.context['REDIRECTIONS'] = self.configure_redirections(
self.url_map)
+
+ # Add tag redirects
+ for tag in self.all_tags:
+ try:
+ tag_str = tag.decode('utf8')
+ except AttributeError:
+ tag_str = tag
+ tag = utils.slugify(tag_str)
+ src_url = '{}tag/{}'.format(self.context['SITE_URL'], tag)
+ dst_url = self.site.link('tag', tag)
+ if src_url != dst_url:
+ self.url_map[src_url] = dst_url
+
self.write_urlmap_csv(
os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
rendered_template = conf_template.render(**prepare_config(self.context))
@@ -186,26 +208,6 @@ class CommandImportWordpress(Command, ImportMixin):
rendered_template)
@classmethod
- def _glue_xml_lines(cls, xml):
- new_xml = xml[0]
- previous_line_ended_in_newline = new_xml.endswith(b'\n')
- previous_line_was_indentet = False
- for line in xml[1:]:
- if (re.match(b'^[ \t]+', line) and previous_line_ended_in_newline):
- new_xml = b''.join((new_xml, line))
- previous_line_was_indentet = True
- elif previous_line_was_indentet:
- new_xml = b''.join((new_xml, line))
- previous_line_was_indentet = False
- else:
- new_xml = b'\n'.join((new_xml, line))
- previous_line_was_indentet = False
-
- previous_line_ended_in_newline = line.endswith(b'\n')
-
- return new_xml
-
- @classmethod
def read_xml_file(cls, filename):
xml = []
@@ -215,8 +217,7 @@ class CommandImportWordpress(Command, ImportMixin):
if b'<atom:link rel=' in line:
continue
xml.append(line)
-
- return cls._glue_xml_lines(xml)
+ return b'\n'.join(xml)
@classmethod
def get_channel_from_file(cls, filename):
@@ -255,9 +256,15 @@ class CommandImportWordpress(Command, ImportMixin):
'{{{0}}}author_display_name'.format(wordpress_namespace),
"Joe Example")
context['POSTS'] = '''(
+ ("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.txt", "posts", "post.tmpl"),
+ ("posts/*.md", "posts", "post.tmpl"),
("posts/*.wp", "posts", "post.tmpl"),
)'''
context['PAGES'] = '''(
+ ("stories/*.rst", "stories", "story.tmpl"),
+ ("stories/*.txt", "stories", "story.tmpl"),
+ ("stories/*.md", "stories", "story.tmpl"),
("stories/*.wp", "stories", "story.tmpl"),
)'''
context['COMPILERS'] = '''{
@@ -274,8 +281,12 @@ class CommandImportWordpress(Command, ImportMixin):
return
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))
+ return
with open(dst_path, 'wb+') as fd:
- fd.write(requests.get(url).content)
+ fd.write(request.content)
except requests.exceptions.ConnectionError as err:
LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err))
@@ -285,8 +296,7 @@ class CommandImportWordpress(Command, ImportMixin):
link = get_text_tag(item, '{{{0}}}link'.format(wordpress_namespace),
'foo')
path = urlparse(url).path
- dst_path = os.path.join(*([self.output_folder, 'files']
- + list(path.split('/'))))
+ dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/'))))
dst_dir = os.path.dirname(dst_path)
utils.makedirs(dst_dir)
LOGGER.info("Downloading {0} => {1}".format(url, dst_path))
@@ -306,7 +316,6 @@ class CommandImportWordpress(Command, ImportMixin):
return
additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace))
-
if additional_metadata is None:
return
@@ -341,8 +350,7 @@ class CommandImportWordpress(Command, ImportMixin):
url = '/'.join([source_path, filename.decode('utf-8')])
path = urlparse(url).path
- dst_path = os.path.join(*([self.output_folder, 'files']
- + list(path.split('/'))))
+ dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/'))))
dst_dir = os.path.dirname(dst_path)
utils.makedirs(dst_dir)
LOGGER.info("Downloading {0} => {1}".format(url, dst_path))
@@ -351,13 +359,34 @@ class CommandImportWordpress(Command, ImportMixin):
links[url] = '/' + dst_url
links[url] = '/' + dst_url
- @staticmethod
- def transform_sourcecode(content):
- new_content = re.sub('\[sourcecode language="([^"]+)"\]',
- "\n~~~~~~~~~~~~{.\\1}\n", content)
- new_content = new_content.replace('[/sourcecode]',
- "\n~~~~~~~~~~~~\n")
- return new_content
+ code_re1 = re.compile(r'\[code.* lang.*?="(.*?)?".*\](.*?)\[/code\]', re.DOTALL | re.MULTILINE)
+ code_re2 = re.compile(r'\[sourcecode.* lang.*?="(.*?)?".*\](.*?)\[/sourcecode\]', re.DOTALL | re.MULTILINE)
+ code_re3 = re.compile(r'\[code.*?\](.*?)\[/code\]', re.DOTALL | re.MULTILINE)
+ code_re4 = re.compile(r'\[sourcecode.*?\](.*?)\[/sourcecode\]', re.DOTALL | re.MULTILINE)
+
+ def transform_code(self, content):
+ # http://en.support.wordpress.com/code/posting-source-code/. There are
+ # a ton of things not supported here. We only do a basic [code
+ # lang="x"] -> ```x translation, and remove quoted html entities (<,
+ # >, &, and ").
+ def replacement(m, c=content):
+ if len(m.groups()) == 1:
+ language = ''
+ code = m.group(0)
+ else:
+ language = m.group(1) or ''
+ code = m.group(2)
+ code = code.replace('&amp;', '&')
+ code = code.replace('&gt;', '>')
+ code = code.replace('&lt;', '<')
+ code = code.replace('&quot;', '"')
+ return '```{language}\n{code}\n```'.format(language=language, code=code)
+
+ content = self.code_re1.sub(replacement, content)
+ content = self.code_re2.sub(replacement, content)
+ content = self.code_re3.sub(replacement, content)
+ content = self.code_re4.sub(replacement, content)
+ return content
@staticmethod
def transform_caption(content):
@@ -374,10 +403,10 @@ class CommandImportWordpress(Command, ImportMixin):
return content
def transform_content(self, content):
- new_content = self.transform_sourcecode(content)
- new_content = self.transform_caption(new_content)
- new_content = self.transform_multiple_newlines(new_content)
- return new_content
+ content = self.transform_code(content)
+ content = self.transform_caption(content)
+ content = self.transform_multiple_newlines(content)
+ return content
def import_item(self, item, wordpress_namespace, out_folder=None):
"""Takes an item from the feed and creates a post file."""
@@ -391,11 +420,10 @@ class CommandImportWordpress(Command, ImportMixin):
parsed = urlparse(link)
path = unquote(parsed.path.strip('/'))
- # In python 2, path is a str. slug requires a unicode
- # object. According to wikipedia, unquoted strings will
- # usually be UTF8
- if isinstance(path, utils.bytes_str):
+ try:
path = path.decode('utf8')
+ except AttributeError:
+ pass
# Cut out the base directory.
if path.startswith(self.base_dir.strip('/')):
@@ -420,7 +448,13 @@ class CommandImportWordpress(Command, ImportMixin):
description = get_text_tag(item, 'description', '')
post_date = get_text_tag(
item, '{{{0}}}post_date'.format(wordpress_namespace), None)
- dt = utils.to_datetime(post_date)
+ try:
+ dt = utils.to_datetime(post_date)
+ except ValueError:
+ dt = datetime.datetime(1970, 1, 1, 0, 0, 0)
+ LOGGER.error('Malformed date "{0}" in "{1}" [{2}], assuming 1970-01-01 00:00:00 instead.'.format(post_date, title, slug))
+ post_date = dt.strftime('%Y-%m-%d %H:%M:%S')
+
if dt.tzinfo and self.timezone is None:
self.timezone = utils.get_tzname(dt)
status = get_text_tag(
@@ -443,12 +477,20 @@ class CommandImportWordpress(Command, ImportMixin):
if text == 'Uncategorized':
continue
tags.append(text)
+ self.all_tags.add(text)
if '$latex' in content:
tags.append('mathjax')
+ # Find post format if it's there
+ post_format = 'wp'
+ format_tag = [x for x in item.findall('*//{%s}meta_key' % wordpress_namespace) if x.text == '_tc_post_format']
+ if format_tag:
+ post_format = format_tag[0].getparent().find('{%s}meta_value' % wordpress_namespace).text
+
if is_draft and self.exclude_drafts:
LOGGER.notice('Draft "{0}" will not be imported.'.format(title))
+
elif content.strip():
# If no content is found, no files are written.
self.url_map[link] = (self.context['SITE_URL'] +
@@ -475,7 +517,8 @@ class CommandImportWordpress(Command, ImportMixin):
out_meta_filename = slug + '.meta'
out_content_filename = slug + '.wp'
meta_slug = slug
- content = self.transform_content(content)
+ if post_format == 'wp':
+ content = self.transform_content(content)
self.write_metadata(os.path.join(self.output_folder, out_folder,
out_meta_filename),
title, meta_slug, post_date, description, tags)
@@ -510,7 +553,7 @@ def get_text_tag(tag, name, default):
if tag is None:
return default
t = tag.find(name)
- if t is not None:
+ if t is not None and t.text is not None:
return t.text
else:
return default
diff --git a/nikola/plugins/command/init.plugin b/nikola/plugins/command/init.plugin
index a539f51..850dba9 100644
--- a/nikola/plugins/command/init.plugin
+++ b/nikola/plugins/command/init.plugin
@@ -4,6 +4,6 @@ Module = init
[Documentation]
Author = Roberto Alsina
-Version = 0.2
+Version = 1.0
Website = http://getnikola.com
Description = Create a new site.
diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py
index a8b60db..7a36894 100644
--- a/nikola/plugins/command/init.py
+++ b/nikola/plugins/command/init.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -33,11 +33,13 @@ import textwrap
import datetime
import unidecode
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_RSS_READ_MORE_LINK, LEGAL_VALUES
+from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN, DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_RSS_READ_MORE_LINK, LEGAL_VALUES, urlsplit, urlunsplit
from nikola.plugin_categories import Command
from nikola.utils import ask, ask_yesno, get_logger, makedirs, STDERR_HANDLER, load_messages
from nikola.packages.tzlocal import get_localzone
@@ -48,9 +50,10 @@ LOGGER = get_logger('init', STDERR_HANDLER)
SAMPLE_CONF = {
'BLOG_AUTHOR': "Your Name",
'BLOG_TITLE': "Demo Site",
- 'SITE_URL': "http://getnikola.com/",
+ 'SITE_URL': "https://example.com/",
'BLOG_EMAIL': "joe@demo.site",
'BLOG_DESCRIPTION': "This is a demo site for Nikola.",
+ 'PRETTY_URLS': False,
'DEFAULT_LANG': "en",
'TRANSLATIONS': """{
DEFAULT_LANG: "",
@@ -186,7 +189,7 @@ def format_navigation_links(additional_languages, default_lang, messages):
pairs.append(f.format('DEFAULT_LANG', '', get_msg(default_lang)))
for l in additional_languages:
- pairs.append(f.format(json.dumps(l), '/' + l, get_msg(l)))
+ pairs.append(f.format(json.dumps(l, ensure_ascii=False), '/' + l, get_msg(l)))
return u'{{\n{0}\n}}'.format('\n\n'.join(pairs))
@@ -196,11 +199,13 @@ def format_navigation_links(additional_languages, default_lang, messages):
def prepare_config(config):
"""Parse sample config with JSON."""
p = config.copy()
- p.update(dict((k, json.dumps(v)) for k, v in p.items()
- if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'RSS_READ_MORE_LINK')))
+ p.update(dict((k, json.dumps(v, ensure_ascii=False)) for k, v in p.items()
+ if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'RSS_READ_MORE_LINK', 'PRETTY_URLS')))
# READ_MORE_LINKs require some special treatment.
p['INDEX_READ_MORE_LINK'] = "'" + p['INDEX_READ_MORE_LINK'].replace("'", "\\'") + "'"
p['RSS_READ_MORE_LINK'] = "'" + p['RSS_READ_MORE_LINK'].replace("'", "\\'") + "'"
+ # json would make that `true` instead of `True`
+ p['PRETTY_URLS'] = str(p['PRETTY_URLS'])
return p
@@ -237,14 +242,20 @@ class CommandInit(Command):
src = resource_filename('nikola', os.path.join('data', 'samplesite'))
shutil.copytree(src, target)
- @classmethod
- def create_configuration(cls, target):
+ @staticmethod
+ def create_configuration(target):
template_path = resource_filename('nikola', 'conf.py.in')
conf_template = Template(filename=template_path)
conf_path = os.path.join(target, 'conf.py')
with io.open(conf_path, 'w+', encoding='utf8') as fd:
fd.write(conf_template.render(**prepare_config(SAMPLE_CONF)))
+ @staticmethod
+ def create_configuration_to_string():
+ template_path = resource_filename('nikola', 'conf.py.in')
+ conf_template = Template(filename=template_path)
+ return conf_template.render(**prepare_config(SAMPLE_CONF))
+
@classmethod
def create_empty_site(cls, target):
for folder in ('files', 'galleries', 'listings', 'posts', 'stories'):
@@ -253,6 +264,39 @@ class CommandInit(Command):
@staticmethod
def ask_questions(target):
"""Ask some questions about Nikola."""
+ def urlhandler(default, toconf):
+ answer = ask('Site URL', 'https://example.com/')
+ try:
+ answer = answer.decode('utf-8')
+ except (AttributeError, UnicodeDecodeError):
+ pass
+ if not answer.startswith(u'http'):
+ print(" ERROR: You must specify a protocol (http or https).")
+ urlhandler(default, toconf)
+ return
+ if not answer.endswith('/'):
+ print(" The URL does not end in '/' -- adding it.")
+ answer += '/'
+
+ dst_url = urlsplit(answer)
+ try:
+ dst_url.netloc.encode('ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ # The IDN contains characters beyond ASCII. We must convert it
+ # to Punycode. (Issue #1644)
+ nl = dst_url.netloc.encode('idna')
+ answer = urlunsplit((dst_url.scheme,
+ nl,
+ dst_url.path,
+ dst_url.query,
+ dst_url.fragment))
+ print(" Converting to Punycode:", answer)
+
+ SAMPLE_CONF['SITE_URL'] = answer
+
+ def prettyhandler(default, toconf):
+ SAMPLE_CONF['PRETTY_URLS'] = ask_yesno('Enable pretty URLs (/page/ instead of /page.html) that don’t need web server configuration?', default=True)
+
def lhandler(default, toconf, show_header=True):
if show_header:
print("We will now ask you to provide the list of languages you want to use.")
@@ -297,7 +341,7 @@ class CommandInit(Command):
lhandler(default, toconf, show_header=False)
def tzhandler(default, toconf):
- print("\nPlease choose the correct time zone for your blog. Nikola uses the tz database.")
+ print("\nPlease choose the correct time zone for your blog. Nikola uses the tz database.")
print("You can find your time zone here:")
print("http://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
print("")
@@ -309,12 +353,26 @@ class CommandInit(Command):
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]
+ print(" Picking '{0}'.".format(answer))
+ elif len(zonenames) > 1:
+ print(" The following time zones match your query:")
+ print(' ' + '\n '.join(zonenames))
+ continue
+
if tz is not None:
time = datetime.datetime.now(tz).strftime('%H:%M:%S')
print(" Current time in {0}: {1}".format(answer, time))
answered = ask_yesno("Use this time zone?", True)
else:
- print(" ERROR: Time zone not found. Please try again. Time zones are case-sensitive.")
+ print(" ERROR: No matches found. Please try again.")
SAMPLE_CONF['TIMEZONE'] = answer
@@ -353,7 +411,8 @@ class CommandInit(Command):
('Site author', 'Nikola Tesla', True, 'BLOG_AUTHOR'),
('Site author\'s e-mail', 'n.tesla@example.com', True, 'BLOG_EMAIL'),
('Site description', 'This is a demo site for Nikola.', True, 'BLOG_DESCRIPTION'),
- ('Site URL', 'http://getnikola.com/', True, 'SITE_URL'),
+ (urlhandler, None, True, True),
+ (prettyhandler, None, True, True),
('Questions about languages and locales', None, None, None),
(lhandler, None, True, True),
(tzhandler, None, True, True),
@@ -377,6 +436,10 @@ class CommandInit(Command):
query(default, toconf)
else:
answer = ask(query, default)
+ try:
+ answer = answer.decode('utf-8')
+ except (AttributeError, UnicodeDecodeError):
+ pass
if toconf:
SAMPLE_CONF[destination] = answer
if destination == '!target':
@@ -386,7 +449,7 @@ class CommandInit(Command):
STORAGE['target'] = answer
print("\nThat's it, Nikola is now configured. Make sure to edit conf.py to your liking.")
- print("If you are looking for themes and addons, check out http://themes.getnikola.com/ and http://plugins.getnikola.com/.")
+ print("If you are looking for themes and addons, check out https://themes.getnikola.com/ and https://plugins.getnikola.com/.")
print("Have fun!")
return STORAGE
diff --git a/nikola/plugins/command/install_theme.plugin b/nikola/plugins/command/install_theme.plugin
index 84b2623..54a91ff 100644
--- a/nikola/plugins/command/install_theme.plugin
+++ b/nikola/plugins/command/install_theme.plugin
@@ -4,7 +4,7 @@ Module = install_theme
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Install a theme into the current site.
diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py
index 5397772..4937509 100644
--- a/nikola/plugins/command/install_theme.py
+++ b/nikola/plugins/command/install_theme.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,42 +28,18 @@ from __future__ import print_function
import os
import io
import json
-import shutil
+import requests
import pygments
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
-try:
- import requests
-except ImportError:
- requests = None # NOQA
-
from nikola.plugin_categories import Command
from nikola import utils
LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER)
-# Stolen from textwrap in Python 3.3.2.
-def indent(text, prefix, predicate=None): # NOQA
- """Adds 'prefix' to the beginning of selected lines in 'text'.
-
- If 'predicate' is provided, 'prefix' will only be added to the lines
- where 'predicate(line)' is True. If 'predicate' is not provided,
- it will default to adding 'prefix' to all non-empty lines that do not
- consist solely of whitespace characters.
- """
- if predicate is None:
- def predicate(line):
- return line.strip()
-
- def prefixed_lines():
- for line in text.splitlines(True):
- yield (prefix + line if predicate(line) else line)
- return ''.join(prefixed_lines())
-
-
class CommandInstallTheme(Command):
"""Install a theme."""
@@ -86,16 +62,21 @@ class CommandInstallTheme(Command):
'long': 'url',
'type': str,
'help': "URL for the theme repository (default: "
- "http://themes.getnikola.com/v7/themes.json)",
- 'default': 'http://themes.getnikola.com/v7/themes.json'
+ "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."""
- if requests is None:
- utils.req_missing(['requests'], 'install themes')
-
listing = options['list']
url = options['url']
if args:
@@ -103,6 +84,14 @@ class CommandInstallTheme(Command):
else:
name = None
+ if options['getpath'] and name:
+ path = utils.get_theme_path(name)
+ if path:
+ print(path)
+ else:
+ print('not installed')
+ return 0
+
if name is None and not listing:
LOGGER.error("This command needs either a theme name or the -l option.")
return False
@@ -135,36 +124,31 @@ class CommandInstallTheme(Command):
def do_install(self, name, data):
if name in data:
utils.makedirs(self.output_dir)
- LOGGER.info('Downloading: ' + data[name])
+ LOGGER.info("Downloading '{0}'".format(data[name]))
zip_file = io.BytesIO()
zip_file.write(requests.get(data[name]).content)
- LOGGER.info('Extracting: {0} into themes'.format(name))
+ LOGGER.info("Extracting '{0}' into themes/".format(name))
utils.extract_all(zip_file)
- dest_path = os.path.join('themes', name)
+ dest_path = os.path.join(self.output_dir, name)
else:
+ dest_path = os.path.join(self.output_dir, name)
try:
theme_path = utils.get_theme_path(name)
- except:
- LOGGER.error("Can't find theme " + name)
- return False
+ LOGGER.error("Theme '{0}' is already installed in {1}".format(name, theme_path))
+ except Exception:
+ LOGGER.error("Can't find theme {0}".format(name))
- utils.makedirs(self.output_dir)
- dest_path = os.path.join(self.output_dir, name)
- if os.path.exists(dest_path):
- LOGGER.error("{0} is already installed".format(name))
- return False
+ return False
- LOGGER.info('Copying {0} into themes'.format(theme_path))
- shutil.copytree(theme_path, dest_path)
confpypath = os.path.join(dest_path, 'conf.py.sample')
if os.path.exists(confpypath):
LOGGER.notice('This 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:
if self.site.colorful:
- print(indent(pygments.highlight(
+ print(utils.indent(pygments.highlight(
fh.read(), PythonLexer(), TerminalFormatter()),
4 * ' '))
else:
- print(indent(fh.read(), 4 * ' '))
+ print(utils.indent(fh.read(), 4 * ' '))
return True
diff --git a/nikola/plugins/command/new_page.plugin b/nikola/plugins/command/new_page.plugin
index 1f1c84c..f078dd6 100644
--- a/nikola/plugins/command/new_page.plugin
+++ b/nikola/plugins/command/new_page.plugin
@@ -4,6 +4,6 @@ Module = new_page
[Documentation]
Author = Roberto Alsina, Chris Warrick
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create a new page.
diff --git a/nikola/plugins/command/new_page.py b/nikola/plugins/command/new_page.py
index f07ba39..39a85bd 100644
--- a/nikola/plugins/command/new_page.py
+++ b/nikola/plugins/command/new_page.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina, Chris Warrick and others.
+# Copyright © 2012-2015 Roberto Alsina, Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -45,6 +45,14 @@ class CommandNewPage(Command):
'help': 'Title for the page.'
},
{
+ 'name': 'author',
+ 'short': 'a',
+ 'long': 'author',
+ 'type': str,
+ 'default': '',
+ 'help': 'Author of the post.'
+ },
+ {
'name': 'onefile',
'short': '1',
'type': bool,
@@ -71,13 +79,29 @@ class CommandNewPage(Command):
'long': 'format',
'type': str,
'default': '',
- 'help': 'Markup format for the page, one of rest, markdown, wiki, '
- 'bbcode, html, textile, txt2tags',
+ 'help': 'Markup format for the page (use --available-formats for list)',
+ },
+ {
+ 'name': 'available-formats',
+ 'short': 'F',
+ 'long': 'available-formats',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List all available input formats'
+ },
+ {
+ 'name': 'import',
+ 'short': 'i',
+ 'long': 'import',
+ 'type': str,
+ 'default': '',
+ 'help': 'Import an existing file instead of creating a placeholder'
},
]
def _execute(self, options, args):
"""Create a new page."""
+ # Defaults for some values that don’t apply to pages and the is_page option (duh!)
options['tags'] = ''
options['schedule'] = False
options['is_page'] = True
diff --git a/nikola/plugins/command/new_post.plugin b/nikola/plugins/command/new_post.plugin
index ec35c35..fec4b1d 100644
--- a/nikola/plugins/command/new_post.plugin
+++ b/nikola/plugins/command/new_post.plugin
@@ -4,7 +4,7 @@ Module = new_post
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create a new post.
diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py
index 24c09d0..5141c7e 100644
--- a/nikola/plugins/command/new_post.py
+++ b/nikola/plugins/command/new_post.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,6 +30,7 @@ import datetime
import os
import sys
import subprocess
+import operator
from blinker import signal
import dateutil.tz
@@ -37,12 +38,13 @@ import dateutil.tz
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)
LOGGER = POSTLOGGER
-def filter_post_pages(compiler, is_post, compilers, post_pages):
+def filter_post_pages(compiler, is_post, compilers, post_pages, compiler_objs, compilers_raw):
"""Given a compiler ("markdown", "rest"), and whether it's meant for
a post or a page, and compilers, return the correct entry from
post_pages."""
@@ -51,7 +53,15 @@ def filter_post_pages(compiler, is_post, compilers, post_pages):
filtered = [entry for entry in post_pages if entry[3] == is_post]
# These are the extensions supported by the required format
- extensions = compilers[compiler]
+ extensions = compilers.get(compiler)
+ if extensions is None:
+ if compiler in compiler_objs:
+ LOGGER.error("There is a {0} compiler available, but it's not set in your COMPILERS option.".format(compiler))
+ LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK))
+ else:
+ LOGGER.error('Unknown format {0}'.format(compiler))
+ print_compilers(compilers_raw, post_pages, compiler_objs)
+ return False
# Throw away the post_pages with the wrong extensions
filtered = [entry for entry in filtered if any([ext in entry[0] for ext in
@@ -59,13 +69,77 @@ def filter_post_pages(compiler, is_post, compilers, post_pages):
if not filtered:
type_name = "post" if is_post else "page"
- raise Exception("Can't find a way, using your configuration, to create "
- "a {0} in format {1}. You may want to tweak "
- "COMPILERS or {2}S in conf.py".format(
- type_name, compiler, type_name.upper()))
+ LOGGER.error("Can't find a way, using your configuration, to create "
+ "a {0} in format {1}. You may want to tweak "
+ "COMPILERS or {2}S in conf.py".format(
+ type_name, compiler, type_name.upper()))
+ LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK))
+
+ return False
return filtered[0]
+def print_compilers(compilers_raw, post_pages, compiler_objs):
+ """
+ List all available compilers in a human-friendly format.
+
+ :param compilers_raw: The compilers dict, mapping compiler names to tuples of extensions
+ :param post_pages: The post_pages structure
+ :param compilers_objs: Compiler objects
+ """
+
+ # We use compilers_raw, because the normal dict can contain
+ # garbage coming from the translation candidate implementation.
+ # Entries are in format: (name, extensions, used_in_post_pages)
+ parsed_compilers = {'used': [], 'unused': [], 'disabled': []}
+
+ for compiler_name, compiler_obj in compiler_objs.items():
+ fname = compiler_obj.friendly_name or compiler_name
+ if compiler_name not in compilers_raw:
+ parsed_compilers['disabled'].append((compiler_name, fname, (), False))
+ else:
+ # stolen from filter_post_pages
+ extensions = compilers_raw[compiler_name]
+ filtered = [entry for entry in post_pages if any(
+ [ext in entry[0] for ext in extensions])]
+ if filtered:
+ parsed_compilers['used'].append((compiler_name, fname, extensions, True))
+ else:
+ parsed_compilers['unused'].append((compiler_name, fname, extensions, False))
+
+ # Sort compilers alphabetically by name, just so it’s prettier (and
+ # deterministic)
+ parsed_compilers['used'].sort(key=operator.itemgetter(0))
+ parsed_compilers['unused'].sort(key=operator.itemgetter(0))
+ parsed_compilers['disabled'].sort(key=operator.itemgetter(0))
+
+ # We also group the compilers by status for readability.
+ parsed_list = parsed_compilers['used'] + parsed_compilers['unused'] + parsed_compilers['disabled']
+
+ print("Available input formats:\n")
+
+ name_width = max([len(i[0]) for i in parsed_list] + [4]) # 4 == len('NAME')
+ fname_width = max([len(i[1]) for i in parsed_list] + [11]) # 11 == len('DESCRIPTION')
+
+ print((' {0:<' + str(name_width) + '} {1:<' + str(fname_width) + '} EXTENSIONS\n').format('NAME', 'DESCRIPTION'))
+
+ for name, fname, extensions, used in parsed_list:
+ flag = ' ' if used else '!'
+ flag = flag if extensions else '~'
+
+ extensions = ', '.join(extensions) if extensions else '(disabled: not in COMPILERS)'
+
+ print(('{flag}{name:<' + str(name_width) + '} {fname:<' + str(fname_width) + '} {extensions}').format(flag=flag, name=name, fname=fname, extensions=extensions))
+
+ print("""
+More compilers are available in the Plugins Index.
+
+Compilers marked with ! and ~ require additional configuration:
+ ! not in the PAGES/POSTS tuples (unused)
+ ~ not in the COMPILERS dict (disabled)
+Read more: {0}""".format(COMPILERS_DOC_LINK))
+
+
def get_default_compiler(is_post, compilers, post_pages):
"""Given compilers and post_pages, return a reasonable
default compiler for this kind of post/page.
@@ -116,7 +190,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False):
rrule = None # NOQA
if schedule and rrule and rule:
try:
- rule_ = rrule.rrulestr(rule, dtstart=last_date)
+ rule_ = rrule.rrulestr(rule, dtstart=last_date or date)
except Exception:
LOGGER.error('Unable to parse rule string, using current time.')
else:
@@ -161,6 +235,14 @@ class CommandNewPost(Command):
'help': 'Title for the post.'
},
{
+ 'name': 'author',
+ 'short': 'a',
+ 'long': 'author',
+ 'type': str,
+ 'default': '',
+ 'help': 'Author of the post.'
+ },
+ {
'name': 'tags',
'long': 'tags',
'type': str,
@@ -194,8 +276,15 @@ class CommandNewPost(Command):
'long': 'format',
'type': str,
'default': '',
- 'help': 'Markup format for the post, one of rest, markdown, wiki, '
- 'bbcode, html, textile, txt2tags',
+ 'help': 'Markup format for the post (use --available-formats for list)',
+ },
+ {
+ 'name': 'available-formats',
+ 'short': 'F',
+ 'long': 'available-formats',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List all available input formats'
},
{
'name': 'schedule',
@@ -204,6 +293,14 @@ class CommandNewPost(Command):
'default': False,
'help': 'Schedule the post based on recurrence rule'
},
+ {
+ 'name': 'import',
+ 'short': 'i',
+ 'long': 'import',
+ 'type': str,
+ 'default': '',
+ 'help': 'Import an existing file instead of creating a placeholder'
+ },
]
@@ -228,9 +325,16 @@ class CommandNewPost(Command):
is_post = not is_page
content_type = 'page' if is_page else 'post'
title = options['title'] or None
+ author = options['author'] or ''
tags = options['tags']
onefile = options['onefile']
twofile = options['twofile']
+ import_file = options['import']
+ wants_available = options['available-formats']
+
+ if wants_available:
+ print_compilers(self.site.config['_COMPILERS_RAW'], self.site.config['post_pages'], self.site.compilers)
+ return
if is_page:
LOGGER = PAGELOGGER
@@ -243,6 +347,10 @@ class CommandNewPost(Command):
onefile = self.site.config.get('ONE_FILE_POSTS', True)
content_format = options['content_format']
+ content_subformat = None
+
+ if "@" in content_format:
+ content_format, content_subformat = content_format.split("@")
if not content_format: # Issue #400
content_format = get_default_compiler(
@@ -251,7 +359,8 @@ class CommandNewPost(Command):
self.site.config['post_pages'])
if content_format not in compiler_names:
- LOGGER.error("Unknown {0} format {1}".format(content_type, content_format))
+ LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin?".format(content_type, content_format))
+ print_compilers(self.site.config['_COMPILERS_RAW'], self.site.config['post_pages'], self.site.compilers)
return
compiler_plugin = self.site.plugin_manager.getPluginByName(
content_format, "PageCompiler").plugin_object
@@ -259,10 +368,19 @@ class CommandNewPost(Command):
# Guess where we should put this
entry = filter_post_pages(content_format, is_post,
self.site.config['COMPILERS'],
- self.site.config['post_pages'])
+ self.site.config['post_pages'],
+ self.site.compilers,
+ self.site.config['_COMPILERS_RAW'])
+
+ if entry is False:
+ return 1
- print("Creating New {0}".format(content_type.title()))
- print("-----------------\n")
+ if import_file:
+ print("Importing Existing {xx}".format(xx=content_type.title()))
+ print("-----------------------\n")
+ else:
+ print("Creating New {xx}".format(xx=content_type.title()))
+ print("-----------------\n")
if title is not None:
print("Title:", title)
else:
@@ -272,7 +390,7 @@ class CommandNewPost(Command):
if isinstance(title, utils.bytes_str):
try:
title = title.decode(sys.stdin.encoding)
- except AttributeError: # for tests
+ except (AttributeError, TypeError): # for tests
title = title.decode('utf-8')
title = title.strip()
@@ -282,9 +400,16 @@ class CommandNewPost(Command):
if isinstance(path, utils.bytes_str):
try:
path = path.decode(sys.stdin.encoding)
- except AttributeError: # for tests
+ except (AttributeError, TypeError): # for tests
path = path.decode('utf-8')
slug = utils.slugify(os.path.splitext(os.path.basename(path))[0])
+
+ if isinstance(author, utils.bytes_str):
+ 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']
rule = self.site.config['SCHEDULE_RULE']
@@ -308,23 +433,46 @@ class CommandNewPost(Command):
if not path:
txt_path = os.path.join(output_path, slug + suffix)
else:
- txt_path = path
+ txt_path = os.path.join(self.site.original_cwd, path)
if (not onefile and os.path.isfile(meta_path)) or \
os.path.isfile(txt_path):
+
+ # Emit an event when a post exists
+ event = dict(path=txt_path)
+ if not onefile: # write metadata file
+ event['meta_path'] = meta_path
+ signal('existing_' + content_type).send(self, **event)
+
LOGGER.error("The title already exists!")
- exit()
+ return 8
d_name = os.path.dirname(txt_path)
utils.makedirs(d_name)
- metadata = self.site.config['ADDITIONAL_METADATA']
+ metadata = {}
+ if author:
+ metadata['author'] = author
+ metadata.update(self.site.config['ADDITIONAL_METADATA'])
+ data.update(metadata)
+
+ # ipynb plugin needs the ipython 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
# Override onefile if not really supported.
if not compiler_plugin.supports_onefile and onefile:
onefile = False
LOGGER.warn('This compiler does not support one-file posts.')
- content = "Write your {0} here.".format('page' if is_page else 'post')
+ if import_file:
+ with io.open(import_file, 'r', encoding='utf-8') as fh:
+ content = fh.read()
+ else:
+ if is_page:
+ content = self.site.MESSAGES[self.site.default_lang]["Write your page here."]
+ else:
+ content = self.site.MESSAGES[self.site.default_lang]["Write your post here."]
compiler_plugin.create_post(
txt_path, content=content, onefile=onefile, title=title,
slug=slug, date=date, tags=tags, is_page=is_page, **metadata)
diff --git a/nikola/plugins/command/orphans.plugin b/nikola/plugins/command/orphans.plugin
index 408578b..f491eaf 100644
--- a/nikola/plugins/command/orphans.plugin
+++ b/nikola/plugins/command/orphans.plugin
@@ -4,7 +4,7 @@ Module = orphans
[Documentation]
Author = Roberto Alsina, Chris Warrick
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = List all orphans
diff --git a/nikola/plugins/command/orphans.py b/nikola/plugins/command/orphans.py
index ff114b4..f550e17 100644
--- a/nikola/plugins/command/orphans.py
+++ b/nikola/plugins/command/orphans.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina, Chris Warrick and others.
+# Copyright © 2012-2015 Roberto Alsina, Chris Warrick 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/plugin.plugin b/nikola/plugins/command/plugin.plugin
index d2bca92..2815caa 100644
--- a/nikola/plugins/command/plugin.plugin
+++ b/nikola/plugins/command/plugin.plugin
@@ -4,7 +4,7 @@ Module = plugin
[Documentation]
Author = Roberto Alsina and Chris Warrick
-Version = 0.2
+Version = 1.0
Website = http://getnikola.com
Description = Manage Nikola plugins
diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py
index 71901b8..56eb1d7 100644
--- a/nikola/plugins/command/plugin.py
+++ b/nikola/plugins/command/plugin.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,41 +30,18 @@ import os
import shutil
import subprocess
import sys
+import requests
import pygments
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
-try:
- import requests
-except ImportError:
- requests = None # NOQA
-
from nikola.plugin_categories import Command
from nikola import utils
LOGGER = utils.get_logger('plugin', utils.STDERR_HANDLER)
-# Stolen from textwrap in Python 3.3.2.
-def indent(text, prefix, predicate=None): # NOQA
- """Adds 'prefix' to the beginning of selected lines in 'text'.
-
- If 'predicate' is provided, 'prefix' will only be added to the lines
- where 'predicate(line)' is True. If 'predicate' is not provided,
- it will default to adding 'prefix' to all non-empty lines that do not
- consist solely of whitespace characters.
- """
- if predicate is None:
- def predicate(line):
- return line.strip()
-
- def prefixed_lines():
- for line in text.splitlines(True):
- yield (prefix + line if predicate(line) else line)
- return ''.join(prefixed_lines())
-
-
class CommandPlugin(Command):
"""Manage plugins."""
@@ -105,8 +82,8 @@ class CommandPlugin(Command):
'long': 'url',
'type': str,
'help': "URL for the plugin repository (default: "
- "http://plugins.getnikola.com/v7/plugins.json)",
- 'default': 'http://plugins.getnikola.com/v7/plugins.json'
+ "https://plugins.getnikola.com/v7/plugins.json)",
+ 'default': 'https://plugins.getnikola.com/v7/plugins.json'
},
{
'name': 'user',
@@ -258,7 +235,7 @@ class CommandPlugin(Command):
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:
- print(indent(fh.read(), 4 * ' '))
+ print(utils.indent(fh.read(), 4 * ' '))
print('You have to install those yourself or through a '
'package manager.')
else:
@@ -272,8 +249,8 @@ class CommandPlugin(Command):
with io.open(reqnpypath, 'r', encoding='utf-8') as fh:
for l in fh.readlines():
i, j = l.split('::')
- print(indent(i.strip(), 4 * ' '))
- print(indent(j.strip(), 8 * ' '))
+ print(utils.indent(i.strip(), 4 * ' '))
+ print(utils.indent(j.strip(), 8 * ' '))
print()
print('You have to install those yourself or through a package '
@@ -284,11 +261,11 @@ class CommandPlugin(Command):
print('Contents of the conf.py.sample file:\n')
with io.open(confpypath, 'r', encoding='utf-8') as fh:
if self.site.colorful:
- print(indent(pygments.highlight(
+ print(utils.indent(pygments.highlight(
fh.read(), PythonLexer(), TerminalFormatter()),
4 * ' '))
else:
- print(indent(fh.read(), 4 * ' '))
+ print(utils.indent(fh.read(), 4 * ' '))
return True
def do_uninstall(self, name):
@@ -311,8 +288,6 @@ class CommandPlugin(Command):
return False
def get_json(self, url):
- if requests is None:
- utils.req_missing(['requests'], 'install or list available plugins', python=True, optional=False)
if self.json is None:
self.json = requests.get(url).json()
return self.json
diff --git a/nikola/plugins/command/rst2html.plugin b/nikola/plugins/command/rst2html.plugin
new file mode 100644
index 0000000..0d0d3b0
--- /dev/null
+++ b/nikola/plugins/command/rst2html.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = rst2html
+Module = rst2html
+
+[Documentation]
+Author = Chris Warrick
+Version = 1.0
+Website = http://getnikola.com
+Description = Compile reStructuredText to HTML using the Nikola architecture
diff --git a/nikola/plugins/command/rst2html/__init__.py b/nikola/plugins/command/rst2html/__init__.py
new file mode 100644
index 0000000..342aaeb
--- /dev/null
+++ b/nikola/plugins/command/rst2html/__init__.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2015 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.
+
+from __future__ import unicode_literals, print_function
+
+import io
+import lxml.html
+from pkg_resources import resource_filename
+from mako.template import Template
+from nikola.plugin_categories import Command
+
+
+class CommandRst2Html(Command):
+ """Compile reStructuredText to HTML, using Nikola architecture."""
+
+ name = "rst2html"
+ doc_usage = "infile"
+ doc_purpose = "compile reStructuredText to HTML files"
+ needs_config = False
+
+ def _execute(self, options, args):
+ """Compile reStructuredText to standalone HTML files."""
+ compiler = self.site.plugin_manager.getPluginByName('rest', 'PageCompiler').plugin_object
+ if len(args) != 1:
+ 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:
+ data = in_file.read()
+ output, error_level, deps = compiler.compile_html_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 = fh.read()
+
+ template_path = resource_filename('nikola', 'plugins/command/rst2html/rst2html.tmpl')
+ template = Template(filename=template_path)
+ template_output = template.render(rstcss=rstcss, output=output)
+ parser = lxml.html.HTMLParser(remove_blank_text=True)
+ doc = lxml.html.document_fromstring(template_output, parser)
+ html = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True)
+ print(html)
+ if error_level < 3:
+ return 0
+ else:
+ return 1
diff --git a/nikola/plugins/command/rst2html/rst2html.tmpl b/nikola/plugins/command/rst2html/rst2html.tmpl
new file mode 100644
index 0000000..5a892ea
--- /dev/null
+++ b/nikola/plugins/command/rst2html/rst2html.tmpl
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<style class="text/css">
+${rstcss}
+</style>
+</head>
+
+<body>
+${output}
+</body>
+</html>
diff --git a/nikola/plugins/command/serve.plugin b/nikola/plugins/command/serve.plugin
index e663cc6..0c1176d 100644
--- a/nikola/plugins/command/serve.plugin
+++ b/nikola/plugins/command/serve.plugin
@@ -4,7 +4,7 @@ Module = serve
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Start test server.
diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py
index de4f6e2..0e4d01f 100644
--- a/nikola/plugins/command/serve.py
+++ b/nikola/plugins/command/serve.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +26,7 @@
from __future__ import print_function
import os
+import socket
import webbrowser
try:
from BaseHTTPServer import HTTPServer
@@ -38,6 +39,11 @@ from nikola.plugin_categories import Command
from nikola.utils import get_logger
+class IPv6Server(HTTPServer):
+ """An IPv6 HTTPServer."""
+ address_family = socket.AF_INET6
+
+
class CommandServe(Command):
"""Start test server."""
@@ -53,7 +59,7 @@ class CommandServe(Command):
'long': 'port',
'default': 8000,
'type': int,
- 'help': 'Port nummber (default: 8000)',
+ 'help': 'Port number (default: 8000)',
},
{
'name': 'address',
@@ -61,7 +67,7 @@ class CommandServe(Command):
'long': 'address',
'type': str,
'default': '',
- 'help': 'Address to bind (default: 0.0.0.0 – all local interfaces)',
+ 'help': 'Address to bind (default: 0.0.0.0 – all local IPv4 interfaces)',
},
{
'name': 'browser',
@@ -70,7 +76,15 @@ class CommandServe(Command):
'type': bool,
'default': False,
'help': 'Open the test server in a web browser',
- }
+ },
+ {
+ 'name': 'ipv6',
+ 'short': '6',
+ 'long': 'ipv6',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Use IPv6',
+ },
)
def _execute(self, options, args):
@@ -81,19 +95,33 @@ class CommandServe(Command):
self.logger.error("Missing '{0}' folder?".format(out_dir))
else:
os.chdir(out_dir)
- httpd = HTTPServer((options['address'], options['port']),
- OurHTTPRequestHandler)
+ if '[' in options['address']:
+ options['address'] = options['address'].strip('[').strip(']')
+ ipv6 = True
+ OurHTTP = IPv6Server
+ elif options['ipv6']:
+ ipv6 = True
+ OurHTTP = IPv6Server
+ else:
+ ipv6 = False
+ OurHTTP = HTTPServer
+
+ httpd = OurHTTP((options['address'], options['port']),
+ OurHTTPRequestHandler)
sa = httpd.socket.getsockname()
self.logger.info("Serving HTTP on {0} port {1}...".format(*sa))
if options['browser']:
- server_url = "http://{0}:{1}/".format(*sa)
+ if ipv6:
+ server_url = "http://[{0}]:{1}/".format(*sa)
+ else:
+ server_url = "http://{0}:{1}/".format(*sa)
self.logger.info("Opening {0} in the default web browser...".format(server_url))
webbrowser.open(server_url)
try:
httpd.serve_forever()
except KeyboardInterrupt:
self.logger.info("Server is shutting down.")
- exit(130)
+ return 130
class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
diff --git a/nikola/plugins/command/status.plugin b/nikola/plugins/command/status.plugin
new file mode 100644
index 0000000..e02da8b
--- /dev/null
+++ b/nikola/plugins/command/status.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = status
+Module = status
+
+[Documentation]
+Author = Daniel Aleksandersen
+Version = 1.0
+Website = https://getnikola.com
+Description = Site status
diff --git a/nikola/plugins/command/status.py b/nikola/plugins/command/status.py
new file mode 100644
index 0000000..b8a6a60
--- /dev/null
+++ b/nikola/plugins/command/status.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2015 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+import io
+import os
+from datetime import datetime
+from dateutil.tz import gettz, tzlocal
+
+from nikola.plugin_categories import Command
+
+
+class CommandDeploy(Command):
+ """ Site status. """
+ name = "status"
+
+ doc_purpose = "display site status"
+ doc_description = "Show information about the posts and site deployment."
+ doc_usage = '[-l|--list-drafts] [-m|--list-modified] [-s|--list-scheduled]'
+ logger = None
+ cmd_options = [
+ {
+ 'name': 'list_drafts',
+ 'short': 'd',
+ 'long': 'list-drafts',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List all drafts',
+ },
+ {
+ 'name': 'list_modified',
+ 'short': 'm',
+ 'long': 'list-modified',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List all modified files since last deployment',
+ },
+ {
+ 'name': 'list_scheduled',
+ 'short': 's',
+ 'long': 'list-scheduled',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List all scheduled posts',
+ },
+ ]
+
+ def _execute(self, options, args):
+
+ self.site.scan_posts()
+
+ timestamp_path = os.path.join(self.site.config["CACHE_FOLDER"], "lastdeploy")
+
+ last_deploy = None
+
+ try:
+ with io.open(timestamp_path, "r", encoding="utf8") as inf:
+ last_deploy = datetime.strptime(inf.read().strip(), "%Y-%m-%dT%H:%M:%S.%f")
+ last_deploy_offset = datetime.utcnow() - last_deploy
+ except (IOError, Exception):
+ print("It does not seem like you’ve ever deployed the site (or cache missing).")
+
+ if last_deploy:
+
+ fmod_since_deployment = []
+ for root, dirs, files in os.walk(self.site.config["OUTPUT_FOLDER"], followlinks=True):
+ if not dirs and not files:
+ continue
+ for fname in files:
+ fpath = os.path.join(root, fname)
+ fmodtime = datetime.fromtimestamp(os.stat(fpath).st_mtime)
+ if fmodtime.replace(tzinfo=tzlocal()) > last_deploy.replace(tzinfo=gettz("UTC")).astimezone(tz=tzlocal()):
+ fmod_since_deployment.append(fpath)
+
+ if len(fmod_since_deployment) > 0:
+ print("{0} output files modified since last deployment {1} ago.".format(str(len(fmod_since_deployment)), self.human_time(last_deploy_offset)))
+ if options['list_modified']:
+ for fpath in fmod_since_deployment:
+ print("Modified: '{0}'".format(fpath))
+ else:
+ print("Last deployment {0} ago.".format(self.human_time(last_deploy_offset)))
+
+ now = datetime.utcnow().replace(tzinfo=gettz("UTC"))
+
+ posts_count = len(self.site.all_posts)
+
+ # find all drafts
+ posts_drafts = [post for post in self.site.all_posts if post.is_draft]
+ posts_drafts = sorted(posts_drafts, key=lambda post: post.source_path)
+
+ # find all scheduled posts with offset from now until publishing time
+ posts_scheduled = [(post.date - now, post) for post in self.site.all_posts if post.publish_later]
+ posts_scheduled = sorted(posts_scheduled, key=lambda offset_post: (offset_post[0], offset_post[1].source_path))
+
+ if len(posts_scheduled) > 0:
+ if options['list_scheduled']:
+ for offset, post in posts_scheduled:
+ print("Scheduled: '{1}' ({2}; source: {3}) in {0}".format(self.human_time(offset), post.meta('title'), post.permalink(), post.source_path))
+ else:
+ offset, post = posts_scheduled[0]
+ print("{0} to next scheduled post ('{1}'; {2}; source: {3}).".format(self.human_time(offset), post.meta('title'), post.permalink(), post.source_path))
+ if options['list_drafts']:
+ for post in posts_drafts:
+ print("Draft: '{0}' ({1}; source: {2})".format(post.meta('title'), post.permalink(), post.source_path))
+ print("{0} posts in total, {1} scheduled, and {2} drafts.".format(posts_count, len(posts_scheduled), len(posts_drafts)))
+
+ def human_time(self, dt):
+ days = dt.days
+ hours = dt.seconds / 60 // 60
+ minutes = dt.seconds / 60 - (hours * 60)
+ if days > 0:
+ return "{0:.0f} days and {1:.0f} hours".format(days, hours)
+ elif hours > 0:
+ return "{0:.0f} hours and {1:.0f} minutes".format(hours, minutes)
+ elif minutes:
+ return "{0:.0f} minutes".format(minutes)
+ return False
diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin
index 3c1ae95..a3f58e8 100644
--- a/nikola/plugins/command/version.plugin
+++ b/nikola/plugins/command/version.plugin
@@ -4,6 +4,6 @@ Module = version
[Documentation]
Author = Roberto Alsina
-Version = 0.2
+Version = 1.0
Website = http://getnikola.com
Description = Show nikola version
diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py
index 9b42423..b6520d7 100644
--- a/nikola/plugins/command/version.py
+++ b/nikola/plugins/command/version.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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,42 @@
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'
+
class CommandVersion(Command):
"""Print the version."""
name = "version"
- doc_usage = ""
+ doc_usage = "[--check]"
needs_config = False
doc_purpose = "print the Nikola version number"
+ cmd_options = [
+ {
+ 'name': 'check',
+ 'long': 'check',
+ 'short': '',
+ 'default': False,
+ 'type': bool,
+ 'help': "Check for new versions.",
+ }
+ ]
def _execute(self, options={}, args=None):
"""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__:
+ 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))
diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/compile/__init__.py
+++ b/nikola/plugins/compile/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 21dd338..66623b2 100644
--- a/nikola/plugins/compile/html.plugin
+++ b/nikola/plugins/compile/html.plugin
@@ -4,7 +4,7 @@ Module = html
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Compile HTML into HTML (just copy)
diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py
index 24bf385..ab0c2f6 100644
--- a/nikola/plugins/compile/html.py
+++ b/nikola/plugins/compile/html.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,18 +29,16 @@
from __future__ import unicode_literals
import os
-import re
import io
from nikola.plugin_categories import PageCompiler
from nikola.utils import makedirs, write_metadata
-_META_SEPARATOR = '(' + os.linesep * 2 + '|' + ('\n' * 2) + '|' + ("\r\n" * 2) + ')'
-
class CompileHtml(PageCompiler):
"""Compile HTML into HTML."""
name = "html"
+ friendly_name = "HTML"
def compile_html(self, source, dest, is_two_file=True):
makedirs(os.path.dirname(dest))
@@ -48,7 +46,7 @@ class CompileHtml(PageCompiler):
with io.open(source, "r", encoding="utf8") as in_file:
data = in_file.read()
if not is_two_file:
- data = re.split(_META_SEPARATOR, data, maxsplit=1)[-1]
+ _, data = self.split_metadata(data)
out_file.write(data)
return True
diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin
index e258d8a..efe6702 100644
--- a/nikola/plugins/compile/ipynb.plugin
+++ b/nikola/plugins/compile/ipynb.plugin
@@ -3,8 +3,8 @@ Name = ipynb
Module = ipynb
[Documentation]
-Author = Damian Avila
-Version = 1.0
-Website = http://www.oquanta.info
-Description = Compile IPython notebooks into HTML
+Author = Damian Avila, Chris Warrick and others
+Version = 2.0.0
+Website = http://www.damian.oquanta.info/
+Description = Compile IPython notebooks into Nikola posts
diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py
new file mode 100644
index 0000000..82b76c8
--- /dev/null
+++ b/nikola/plugins/compile/ipynb.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013-2015 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
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on nbconvert."""
+
+from __future__ import unicode_literals, print_function
+import io
+import os
+import sys
+
+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
+ else:
+ import IPython.nbformat.current as nbformat
+ current_nbformat = 'json'
+ kernelspec = None
+
+ from IPython.config import Config
+ flag = True
+except ImportError:
+ flag = None
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing, get_logger
+
+
+class CompileIPynb(PageCompiler):
+ """Compile IPynb into HTML."""
+
+ name = "ipynb"
+ friendly_name = "Jupyter/IPython Notebook"
+ demote_headers = True
+ default_kernel = 'python2' if sys.version_info[0] == 2 else 'python3'
+
+ def set_site(self, site):
+ self.logger = get_logger('compile_ipynb', site.loghandlers)
+ super(CompileIPynb, self).set_site(site)
+
+ def compile_html(self, source, dest, is_two_file=True):
+ if flag is None:
+ req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)')
+ makedirs(os.path.dirname(dest))
+ HTMLExporter.default_template = 'basic'
+ c = Config(self.site.config['IPYNB_CONFIG'])
+ exportHtml = HTMLExporter(config=c)
+ with io.open(dest, "w+", encoding="utf8") as out_file:
+ with io.open(source, "r", encoding="utf8") as in_file:
+ nb_json = nbformat.read(in_file, current_nbformat)
+ (body, resources) = exportHtml.from_notebook_node(nb_json)
+ out_file.write(body)
+
+ def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
+ """read metadata directly from ipynb file.
+
+ As ipynb file support arbitrary metadata as json, the metadata used by Nikola
+ will be assume to be in the 'nikola' subfield.
+ """
+ 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:
+ nb_json = nbformat.read(in_file, current_nbformat)
+ # Metadata might not exist in two-file posts or in hand-crafted
+ # .ipynb files.
+ return nb_json.get('metadata', {}).get('nikola', {})
+
+ def create_post(self, path, **kw):
+ if flag is None:
+ req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)')
+ content = kw.pop('content', None)
+ onefile = kw.pop('onefile', False)
+ kernel = kw.pop('ipython_kernel', None)
+ # is_page is not needed to create the file
+ kw.pop('is_page', False)
+
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+
+ makedirs(os.path.dirname(path))
+
+ if content.startswith("{"):
+ # imported .ipynb file, guaranteed to start with "{" because it’s JSON.
+ nb = nbformat.reads(content, current_nbformat)
+ else:
+ if IPython.version_info[0] >= 3:
+ 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
+
+ if onefile:
+ nb["metadata"]["nikola"] = metadata
+
+ with io.open(path, "w+", encoding="utf8") as fd:
+ if IPython.version_info[0] >= 3:
+ nbformat.write(nb, fd, 4)
+ else:
+ nbformat.write(nb, fd, 'ipynb')
diff --git a/nikola/plugins/compile/ipynb/README.txt b/nikola/plugins/compile/ipynb/README.txt
deleted file mode 100644
index 0a7d6db..0000000
--- a/nikola/plugins/compile/ipynb/README.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-To make this work...
-
-1- You can install the "jinja-site-ipython" theme using this command:
-
-$ nikola install_theme -n jinja-site-ipython
-
-(or xkcd-site-ipython, if you want xkcd styling)
-
-More info here about themes:
-http://getnikola.com/handbook.html#getting-more-themes
-
-OR
-
-You can to download the "jinja-site-ipython" theme from here:
-https://github.com/damianavila/jinja-site-ipython-theme-for-Nikola
-and copy the "site-ipython" folder inside the "themes" folder of your site.
-
-
-2- Then, just add:
-
-post_pages = (
- ("posts/*.ipynb", "posts", "post.tmpl", True),
- ("stories/*.ipynb", "stories", "story.tmpl", False),
-)
-
-and
-
-THEME = 'jinja-site-ipython' (or 'xkcd-site-ipython', if you want xkcd styling)
-
-to your conf.py.
-Finally... to use it:
-
-$nikola new_page -f ipynb
-
-**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
-create one metadata file and the corresponding naive IPython notebook.
-
-$nikola build
-
-And deploy the output folder... to see it locally: $nikola serve
-If you have any doubts, just ask: @damianavila
-
-Cheers.
-Damián
diff --git a/nikola/plugins/compile/ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py
deleted file mode 100644
index 7dde279..0000000
--- a/nikola/plugins/compile/ipynb/__init__.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2013-2014 Damián Avila and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Implementation of compile_html based on nbconvert."""
-
-from __future__ import unicode_literals, print_function
-import io
-import os
-
-try:
- from IPython.nbconvert.exporters import HTMLExporter
- from IPython.nbformat import current as nbformat
- from IPython.config import Config
- flag = True
-except ImportError:
- flag = None
-
-from nikola.plugin_categories import PageCompiler
-from nikola.utils import makedirs, req_missing
-
-
-class CompileIPynb(PageCompiler):
- """Compile IPynb into HTML."""
-
- name = "ipynb"
- supports_onefile = False
- demote_headers = True
-
- def compile_html(self, source, dest, is_two_file=True):
- if flag is None:
- req_missing(['ipython>=1.1.0'], 'build this site (compile ipynb)')
- makedirs(os.path.dirname(dest))
- HTMLExporter.default_template = 'basic'
- c = Config(self.site.config['IPYNB_CONFIG'])
- exportHtml = HTMLExporter(config=c)
- with io.open(dest, "w+", encoding="utf8") as out_file:
- with io.open(source, "r", encoding="utf8") as in_file:
- nb = in_file.read()
- nb_json = nbformat.reads_json(nb)
- (body, resources) = exportHtml.from_notebook_node(nb_json)
- out_file.write(body)
-
- def create_post(self, path, **kw):
- # content and onefile are ignored by ipynb.
- kw.pop('content', None)
- onefile = kw.pop('onefile', False)
- kw.pop('is_page', False)
-
- makedirs(os.path.dirname(path))
- if onefile:
- raise Exception('The one-file format is not supported by this compiler.')
- with io.open(path, "w+", encoding="utf8") as fd:
- fd.write("""{
- "metadata": {
- "name": ""
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
- {
- "cells": [
- {
- "cell_type": "code",
- "collapsed": false,
- "input": [],
- "language": "python",
- "metadata": {},
- "outputs": []
- }
- ],
- "metadata": {}
- }
- ]
-}""")
diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin
index 157579a..a44b798 100644
--- a/nikola/plugins/compile/markdown.plugin
+++ b/nikola/plugins/compile/markdown.plugin
@@ -4,7 +4,7 @@ Module = markdown
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Compile Markdown into HTML
diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py
index 47c7c9b..fbe049d 100644
--- a/nikola/plugins/compile/markdown/__init__.py
+++ b/nikola/plugins/compile/markdown/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,7 +30,6 @@ from __future__ import unicode_literals
import io
import os
-import re
try:
from markdown import markdown
@@ -45,24 +44,27 @@ from nikola.utils import makedirs, req_missing, write_metadata
class CompileMarkdown(PageCompiler):
- """Compile markdown into HTML."""
+ """Compile Markdown into HTML."""
name = "markdown"
+ friendly_name = "Markdown"
demote_headers = True
extensions = []
site = None
def set_site(self, site):
+ self.config_dependencies = []
for plugin_info in site.plugin_manager.getPluginsOfCategory("MarkdownExtension"):
if plugin_info.name in site.config['DISABLED_PLUGINS']:
site.plugin_manager.removePluginFromCategory(plugin_info, "MarkdownExtension")
continue
-
+ self.config_dependencies.append(plugin_info.name)
site.plugin_manager.activatePluginByName(plugin_info.name)
plugin_info.plugin_object.set_site(site)
self.extensions.append(plugin_info.plugin_object)
plugin_info.plugin_object.short_help = plugin_info.description
+ self.config_dependencies.append(str(sorted(site.config.get("MARKDOWN_EXTENSIONS"))))
return super(CompileMarkdown, self).set_site(site)
def compile_html(self, source, dest, is_two_file=True):
@@ -74,7 +76,7 @@ class CompileMarkdown(PageCompiler):
with io.open(source, "r", encoding="utf8") as in_file:
data = in_file.read()
if not is_two_file:
- data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
+ _, data = self.split_metadata(data)
output = markdown(data, self.extensions)
out_file.write(output)
diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py
index 4209bdd..70e7394 100644
--- a/nikola/plugins/compile/markdown/mdx_gist.py
+++ b/nikola/plugins/compile/markdown/mdx_gist.py
@@ -203,14 +203,11 @@ except ImportError:
Extension = Pattern = object
from nikola.plugin_categories import MarkdownExtension
-from nikola.utils import get_logger, req_missing, STDERR_HANDLER
+from nikola.utils import get_logger, STDERR_HANDLER
-LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
+import requests
-try:
- import requests
-except ImportError:
- requests = None # NOQA
+LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
GIST_JS_URL = "https://gist.github.com/{0}.js"
GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
@@ -261,32 +258,27 @@ class GistPattern(Pattern):
gist_elem.set('class', 'gist')
script_elem = etree.SubElement(gist_elem, 'script')
- if requests:
- noscript_elem = etree.SubElement(gist_elem, 'noscript')
-
- try:
- if gist_file:
- script_elem.set('src', GIST_FILE_JS_URL.format(
- gist_id, gist_file))
- raw_gist = (self.get_raw_gist_with_filename(
- gist_id, gist_file))
+ noscript_elem = etree.SubElement(gist_elem, 'noscript')
- else:
- script_elem.set('src', GIST_JS_URL.format(
- gist_id))
- raw_gist = (self.get_raw_gist(gist_id))
+ try:
+ if gist_file:
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+ raw_gist = (self.get_raw_gist_with_filename(
+ gist_id, gist_file))
- # Insert source as <pre/> within <noscript>
- pre_elem = etree.SubElement(noscript_elem, 'pre')
- pre_elem.text = AtomicString(raw_gist)
+ else:
+ script_elem.set('src', GIST_JS_URL.format(gist_id))
+ raw_gist = (self.get_raw_gist(gist_id))
- except GistFetchException as e:
- LOGGER.warn(e.message)
- warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
- noscript_elem.append(warning_comment)
+ # Insert source as <pre/> within <noscript>
+ pre_elem = etree.SubElement(noscript_elem, 'pre')
+ pre_elem.text = AtomicString(raw_gist)
- else:
- req_missing('requests', 'have inline gist source', optional=True)
+ except GistFetchException as e:
+ LOGGER.warn(e.message)
+ warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
+ noscript_elem.append(warning_comment)
return gist_elem
diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py
index ca67511..a03547f 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-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -37,7 +37,6 @@ except ImportError:
from nikola.plugin_categories import MarkdownExtension
-# FIXME: duplicated with listings.py
CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL)
@@ -47,6 +46,7 @@ class NikolaPostProcessor(Postprocessor):
# python-markdown's highlighter uses <div class="codehilite"><pre>
# for code. We switch it to reST's <pre class="code">.
+ # TODO: monkey-patch for CodeHilite that uses nikola.utils.NikolaPygmentsHTML
output = CODERE.sub('<pre class="code literal-block">\\1</pre>', output)
return output
diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py
index 9a67910..670973a 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-2014 Michael Rabbitt, Roberto Alsina and others.
+# Copyright © 2013-2015 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
diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin
index 157b694..ad54b3b 100644
--- a/nikola/plugins/compile/pandoc.plugin
+++ b/nikola/plugins/compile/pandoc.plugin
@@ -4,7 +4,7 @@ Module = pandoc
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Compile markups into HTML using pandoc
diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py
index ada8035..361f158 100644
--- a/nikola/plugins/compile/pandoc.py
+++ b/nikola/plugins/compile/pandoc.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,6 +30,8 @@ You will need, of course, to install pandoc
"""
+from __future__ import unicode_literals
+
import io
import os
import subprocess
@@ -42,11 +44,16 @@ class CompilePandoc(PageCompiler):
"""Compile markups into HTML using pandoc."""
name = "pandoc"
+ friendly_name = "pandoc"
+
+ def set_site(self, site):
+ self.config_dependencies = [str(site.config['PANDOC_OPTIONS'])]
+ super(CompilePandoc, self).set_site(site)
def compile_html(self, source, dest, is_two_file=True):
makedirs(os.path.dirname(dest))
try:
- subprocess.check_call(('pandoc', '-o', dest, source))
+ subprocess.check_call(['pandoc', '-o', dest, source] + self.site.config['PANDOC_OPTIONS'])
except OSError as e:
if e.strreror == 'No such file or directory':
req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False)
diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin
index ac25259..d6623b5 100644
--- a/nikola/plugins/compile/php.plugin
+++ b/nikola/plugins/compile/php.plugin
@@ -4,7 +4,7 @@ Module = php
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Compile PHP into HTML (just copy and name the file .php)
diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py
index 77344fb..bb436e5 100644
--- a/nikola/plugins/compile/php.py
+++ b/nikola/plugins/compile/php.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -40,6 +40,7 @@ class CompilePhp(PageCompiler):
"""Compile PHP into PHP."""
name = "php"
+ friendly_name = "PHP"
def compile_html(self, source, dest, is_two_file=True):
makedirs(os.path.dirname(dest))
diff --git a/nikola/plugins/compile/rest.plugin b/nikola/plugins/compile/rest.plugin
index 55e9c59..f144809 100644
--- a/nikola/plugins/compile/rest.plugin
+++ b/nikola/plugins/compile/rest.plugin
@@ -4,7 +4,7 @@ Module = rest
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Compile reSt into HTML
diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py
index 98c7151..d446fe8 100644
--- a/nikola/plugins/compile/rest/__init__.py
+++ b/nikola/plugins/compile/rest/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,66 +27,78 @@
from __future__ import unicode_literals
import io
import os
-import re
-
-try:
- import docutils.core
- import docutils.nodes
- import docutils.utils
- import docutils.io
- import docutils.readers.standalone
- import docutils.writers.html4css1
- has_docutils = True
-except ImportError:
- has_docutils = False
+
+import docutils.core
+import docutils.nodes
+import docutils.utils
+import docutils.io
+import docutils.readers.standalone
+import docutils.writers.html4css1
from nikola.plugin_categories import PageCompiler
-from nikola.utils import get_logger, makedirs, req_missing, write_metadata
+from nikola.utils import unicode_str, get_logger, makedirs, write_metadata
class CompileRest(PageCompiler):
- """Compile reSt into HTML."""
+ """Compile reStructuredText into HTML."""
name = "rest"
+ friendly_name = "reStructuredText"
demote_headers = True
logger = None
- def compile_html(self, source, dest, is_two_file=True):
- """Compile reSt into HTML."""
+ def _read_extra_deps(self, post):
+ """Reads contents of .dep file and returns them as a list"""
+ dep_path = post.base_path + '.dep'
+ if os.path.isfile(dep_path):
+ with io.open(dep_path, 'r+', encoding='utf8') as depf:
+ deps = [l.strip() for l in depf.readlines()]
+ return deps
+ return []
+
+ def register_extra_dependencies(self, post):
+ """Adds dependency to post object to check .dep file."""
+ post.add_dependency(lambda: self._read_extra_deps(post), 'fragment')
+
+ def compile_html_string(self, data, source_path=None, is_two_file=True):
+ """Compile reSt into HTML strings."""
+ # 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)
+ add_ln = len(m_data.splitlines()) + 1
+
+ default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt')
+ output, error_level, deps = rst2html(
+ data, settings_overrides={
+ 'initial_header_level': 1,
+ 'record_dependencies': True,
+ 'stylesheet_path': None,
+ 'link_stylesheet': True,
+ 'syntax_highlight': 'short',
+ 'math_output': 'mathjax',
+ 'template': default_template_path,
+ }, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms)
+ if not isinstance(output, unicode_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
- if not has_docutils:
- req_missing(['docutils'], 'build this site (compile reStructuredText)')
+ def compile_html(self, source, dest, is_two_file=True):
+ """Compile reSt into HTML files."""
makedirs(os.path.dirname(dest))
error_level = 100
with io.open(dest, "w+", encoding="utf8") as out_file:
with io.open(source, "r", encoding="utf8") as in_file:
data = in_file.read()
- add_ln = 0
- if not is_two_file:
- spl = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)
- data = spl[-1]
- if len(spl) != 1:
- # If errors occur, this will be added to the line
- # number reported by docutils so the line number
- # matches the actual line number (off by 7 with default
- # metadata, could be more or less depending on the post
- # author).
- add_ln = len(spl[0].splitlines()) + 1
-
- default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt')
- output, error_level, deps = rst2html(
- data, settings_overrides={
- 'initial_header_level': 1,
- 'record_dependencies': True,
- 'stylesheet_path': None,
- 'link_stylesheet': True,
- 'syntax_highlight': 'short',
- 'math_output': 'mathjax',
- 'template': default_template_path,
- }, logger=self.logger, source_path=source, l_add_ln=add_ln)
+ output, error_level, deps = self.compile_html_string(data, source, is_two_file)
out_file.write(output)
deps_path = dest + '.dep'
if deps.list:
+ 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:
deps_file.write('\n'.join(deps.list))
else:
@@ -111,15 +123,18 @@ class CompileRest(PageCompiler):
with io.open(path, "w+", encoding="utf8") as fd:
if onefile:
fd.write(write_metadata(metadata))
- fd.write('\n' + content)
+ fd.write('\n')
+ fd.write(content)
def set_site(self, site):
+ self.config_dependencies = []
for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"):
if plugin_info.name in site.config['DISABLED_PLUGINS']:
site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension")
continue
site.plugin_manager.activatePluginByName(plugin_info.name)
+ self.config_dependencies.append(plugin_info.name)
plugin_info.plugin_object.set_site(site)
plugin_info.plugin_object.short_help = plugin_info.description
@@ -160,6 +175,13 @@ def get_observer(settings):
class NikolaReader(docutils.readers.standalone.Reader):
+ def __init__(self, *args, **kwargs):
+ self.transforms = kwargs.pop('transforms', [])
+ docutils.readers.standalone.Reader.__init__(self, *args, **kwargs)
+
+ def get_transforms(self):
+ 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)
@@ -199,7 +221,7 @@ def add_node(node, visit_function=None, depart_function=None):
def depart_Math(self, node):
self.body.append('</math>')
- For full example, you can refer to `Microdata plugin <http://plugins.getnikola.com/#microdata>`_
+ For full example, you can refer to `Microdata plugin <https://plugins.getnikola.com/#microdata>`_
"""
docutils.nodes._add_node_class_names([node.__name__])
if visit_function:
@@ -213,7 +235,7 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
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):
+ 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;
@@ -231,7 +253,7 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
reStructuredText syntax errors.
"""
if reader is None:
- reader = NikolaReader()
+ reader = NikolaReader(transforms=transforms)
# For our custom logging, we have special needs and special settings we
# specify here.
# logger a logger from Nikola
diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py
index 55ddf5c..59b9dc7 100644
--- a/nikola/plugins/compile/rest/chart.py
+++ b/nikola/plugins/compile/rest/chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/rest/doc.py b/nikola/plugins/compile/rest/doc.py
index 6143606..703c234 100644
--- a/nikola/plugins/compile/rest/doc.py
+++ b/nikola/plugins/compile/rest/doc.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/rest/gist.py b/nikola/plugins/compile/rest/gist.py
index 65189b5..ab4d56d 100644
--- a/nikola/plugins/compile/rest/gist.py
+++ b/nikola/plugins/compile/rest/gist.py
@@ -1,16 +1,11 @@
# -*- coding: utf-8 -*-
# This file is public domain according to its author, Brian Hsu
+import requests
from docutils.parsers.rst import Directive, directives
from docutils import nodes
-try:
- import requests
-except ImportError:
- requests = None # NOQA
-
from nikola.plugin_categories import RestExtension
-from nikola.utils import req_missing
class Plugin(RestExtension):
@@ -64,22 +59,15 @@ class GitHubGist(Directive):
if 'file' in self.options:
filename = self.options['file']
- if requests is not None:
- rawGist = (self.get_raw_gist_with_filename(gistID, filename))
+ rawGist = (self.get_raw_gist_with_filename(gistID, filename))
embedHTML = ('<script src="https://gist.github.com/{0}.js'
'?file={1}"></script>').format(gistID, filename)
else:
- if requests is not None:
- rawGist = (self.get_raw_gist(gistID))
+ rawGist = (self.get_raw_gist(gistID))
embedHTML = ('<script src="https://gist.github.com/{0}.js">'
'</script>').format(gistID)
- if requests is None:
- reqnode = nodes.raw(
- '', req_missing('requests', 'have inline gist source',
- optional=True), format='html')
- else:
- reqnode = nodes.literal_block('', rawGist)
+ reqnode = nodes.literal_block('', rawGist)
return [nodes.raw('', embedHTML, format='html'),
nodes.raw('', '<noscript>', format='html'),
diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py
index 23ec254..b8340cf 100644
--- a/nikola/plugins/compile/rest/listing.py
+++ b/nikola/plugins/compile/rest/listing.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,43 +31,95 @@
from __future__ import unicode_literals
import io
import os
+import uuid
try:
from urlparse import urlunsplit
except ImportError:
from urllib.parse import urlunsplit # NOQA
+import docutils.parsers.rst.directives.body
+import docutils.parsers.rst.directives.misc
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
-try:
- from docutils.parsers.rst.directives.body import CodeBlock
-except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
- class CodeBlock(Directive):
- required_arguments = 1
- has_content = True
- option_spec = {}
- CODE = '<pre>{0}</pre>'
-
- def run(self):
- """ Required by the Directive interface. Create docutils nodes """
- return [nodes.raw('', self.CODE.format('\n'.join(self.content)), format='html')]
- directives.register_directive('code', CodeBlock)
+from pygments.lexers import get_lexer_by_name
+import pygments
+import pygments.util
+from nikola import utils
from nikola.plugin_categories import RestExtension
-# Add sphinx compatibility option
-CodeBlock.option_spec['linenos'] = directives.unchanged
-
-class FlexibleCodeBlock(CodeBlock):
+# A sanitized version of docutils.parsers.rst.directives.body.CodeBlock.
+class CodeBlock(Directive):
+ """Parse and mark up content of a code block."""
+ optional_arguments = 1
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'number-lines': directives.unchanged, # integer or None
+ 'linenos': directives.unchanged,
+ 'tab-width': directives.nonnegative_int}
+ has_content = True
def run(self):
+ self.assert_has_content()
+
if 'linenos' in self.options:
self.options['number-lines'] = self.options['linenos']
- return super(FlexibleCodeBlock, self).run()
-CodeBlock = FlexibleCodeBlock
+ if 'tab-width' in self.options:
+ self.content = [x.replace('\t', ' ' * self.options['tab-width']) for x in self.content]
+
+ if self.arguments:
+ language = self.arguments[0]
+ else:
+ language = 'text'
+ set_classes(self.options)
+ classes = ['code']
+ if language:
+ classes.append(language)
+ if 'classes' in self.options:
+ classes.extend(self.options['classes'])
+
+ code = '\n'.join(self.content)
+
+ try:
+ lexer = get_lexer_by_name(language)
+ except pygments.util.ClassNotFound:
+ raise self.error('Cannot find pygments lexer for language "{0}"'.format(language))
+
+ if 'number-lines' in self.options:
+ linenos = 'table'
+ # optional argument `startline`, defaults to 1
+ try:
+ linenostart = int(self.options['number-lines'] or 1)
+ except ValueError:
+ raise self.error(':number-lines: with non-integer start value')
+ else:
+ linenos = False
+ linenostart = 1 # actually unused
+
+ if self.site.invariant: # for testing purposes
+ anchor_ref = 'rest_code_' + 'fixedvaluethatisnotauuid'
+ else:
+ anchor_ref = 'rest_code_' + uuid.uuid4().hex
+
+ formatter = utils.NikolaPygmentsHTML(anchor_ref=anchor_ref, classes=classes, linenos=linenos, linenostart=linenostart)
+ out = pygments.highlight(code, lexer, formatter)
+ node = nodes.raw('', out, format='html')
+
+ self.add_name(node)
+ # if called from "include", set the source
+ if 'source' in self.options:
+ node.attributes['source'] = self.options['source']
+
+ 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
class Plugin(RestExtension):
@@ -79,11 +131,15 @@ class Plugin(RestExtension):
# Even though listings don't use CodeBlock anymore, I am
# leaving these to make the code directive work with
# docutils < 0.9
+ CodeBlock.site = site
+ directives.register_directive('code', CodeBlock)
directives.register_directive('code-block', CodeBlock)
directives.register_directive('sourcecode', CodeBlock)
directives.register_directive('listing', Listing)
+ Listing.folders = site.config['LISTINGS_FOLDERS']
return super(Plugin, self).set_site(site)
+
# Add sphinx compatibility option
listing_spec = Include.option_spec
listing_spec['linenos'] = directives.unchanged
@@ -104,9 +160,17 @@ class Listing(Include):
option_spec = listing_spec
def run(self):
- fname = self.arguments.pop(0)
+ _fname = self.arguments.pop(0)
+ fname = _fname.replace('/', os.sep)
lang = self.arguments.pop(0)
- fpath = os.path.join('listings', fname)
+ 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:
+ fpath = os.path.join(listings_folder, fname) # old syntax: don't specify folder name
+ else:
+ fpath = os.path.join(fname) # must be new syntax: specify folder name
self.arguments.insert(0, fpath)
self.options['code'] = lang
if 'linenos' in self.options:
@@ -114,9 +178,9 @@ class Listing(Include):
with io.open(fpath, 'r+', encoding='utf8') as fileobject:
self.content = fileobject.read().splitlines()
self.state.document.settings.record_dependencies.add(fpath)
- target = urlunsplit(("link", 'listing', fname, '', ''))
+ target = urlunsplit(("link", 'listing', fpath.replace('\\', '/'), '', ''))
generated_nodes = (
- [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
+ [core.publish_doctree('`{0} <{1}>`_'.format(_fname, target))[0]])
generated_nodes += self.get_code_from_file(fileobject)
return generated_nodes
diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py
index ccda559..0363d28 100644
--- a/nikola/plugins/compile/rest/media.py
+++ b/nikola/plugins/compile/rest/media.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py
index f719e31..ddbd82d 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-2014 Udo Spallek, Roberto Alsina and others.
+# Copyright © 2013-2015 Udo Spallek, Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,7 +25,9 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals
+import os
import uuid
+import natsort
from docutils import nodes
from docutils.parsers.rst import Directive, directives
@@ -52,7 +54,7 @@ class PostList(Directive):
Post List
=========
:Directive Arguments: None.
- :Directive Options: lang, start, stop, reverse, tags, template, id
+ :Directive Options: lang, start, stop, reverse, sort, tags, template, id
:Directive Content: None.
Provides a reStructuredText directive to create a list of posts.
@@ -77,6 +79,10 @@ class PostList(Directive):
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).
+
``tags`` : string [, string...]
Filter posts to show only posts having at least one of the ``tags``.
Defaults to None.
@@ -105,6 +111,7 @@ class PostList(Directive):
'start': int,
'stop': int,
'reverse': directives.flag,
+ 'sort': directives.unchanged,
'tags': directives.unchanged,
'slugs': directives.unchanged,
'all': directives.flag,
@@ -124,6 +131,7 @@ class PostList(Directive):
show_all = self.options.get('all', False)
lang = self.options.get('lang', utils.LocaleBorg().current_lang)
template = self.options.get('template', 'post_list_directive.tmpl')
+ sort = self.options.get('sort')
if self.site.invariant: # for testing purposes
post_list_id = self.options.get('id', 'post_list_' + 'fixedvaluethatisnotauuid')
else:
@@ -150,6 +158,9 @@ class PostList(Directive):
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)
+
for post in filtered_timeline[start:stop:step]:
if slugs:
cont = True
@@ -160,10 +171,15 @@ class PostList(Directive):
if cont:
continue
+ bp = post.translated_base_path(lang)
+ if os.path.exists(bp):
+ self.state.document.settings.record_dependencies.add(bp)
+
posts += [post]
if not posts:
return []
+ self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE")
template_data = {
'lang': lang,
diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py
index ea8e413..7826f6a 100644
--- a/nikola/plugins/compile/rest/slides.py
+++ b/nikola/plugins/compile/rest/slides.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/rest/thumbnail.plugin b/nikola/plugins/compile/rest/thumbnail.plugin
new file mode 100644
index 0000000..3b73340
--- /dev/null
+++ b/nikola/plugins/compile/rest/thumbnail.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = rest_thumbnail
+Module = thumbnail
+
+[Documentation]
+Author = Pelle Nilsson
+Version = 0.1
+Website = http://getnikola.com
+Description = reST directive to facilitate enlargeable images with thumbnails
diff --git a/nikola/plugins/compile/rest/thumbnail.py b/nikola/plugins/compile/rest/thumbnail.py
new file mode 100644
index 0000000..5388d8d
--- /dev/null
+++ b/nikola/plugins/compile/rest/thumbnail.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2014-2015 Pelle Nilsson and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import os
+
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.directives.images import Image, Figure
+
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_thumbnail"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('thumbnail', Thumbnail)
+ return super(Plugin, self).set_site(site)
+
+
+class Thumbnail(Figure):
+
+ def align(argument):
+ return directives.choice(argument, Image.align_values)
+
+ def figwidth_value(argument):
+ if argument.lower() == 'image':
+ return 'image'
+ else:
+ return directives.length_or_percentage_or_unitless(argument, 'px')
+
+ option_spec = Image.option_spec.copy()
+ option_spec['figwidth'] = figwidth_value
+ option_spec['figclass'] = directives.class_option
+ has_content = True
+
+ def run(self):
+ uri = directives.uri(self.arguments[0])
+ self.options['target'] = uri
+ self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri))
+ if self.content:
+ (node,) = Figure.run(self)
+ else:
+ (node,) = Image.run(self)
+ return [node]
diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py
index 4b34dfe..bc44b0e 100644
--- a/nikola/plugins/compile/rest/vimeo.py
+++ b/nikola/plugins/compile/rest/vimeo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,15 +28,11 @@
from docutils import nodes
from docutils.parsers.rst import Directive, directives
-try:
- import requests
-except ImportError:
- requests = None # NOQA
+import requests
import json
from nikola.plugin_categories import RestExtension
-from nikola.utils import req_missing
class Plugin(RestExtension):
@@ -94,10 +90,6 @@ class Vimeo(Directive):
return [nodes.raw('', CODE.format(**options), format='html')]
def check_modules(self):
- msg = None
- if requests is None:
- msg = req_missing(['requests'], 'use the vimeo directive', optional=True)
- return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
return None
def set_video_size(self):
diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py
index b32e77a..7c6bba1 100644
--- a/nikola/plugins/compile/rest/youtube.py
+++ b/nikola/plugins/compile/rest/youtube.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/loghandler/__init__.py b/nikola/plugins/loghandler/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/loghandler/__init__.py
+++ b/nikola/plugins/loghandler/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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/loghandler/smtp.plugin b/nikola/plugins/loghandler/smtp.plugin
index e914b3d..38c1d96 100644
--- a/nikola/plugins/loghandler/smtp.plugin
+++ b/nikola/plugins/loghandler/smtp.plugin
@@ -4,6 +4,6 @@ Module = smtp
[Documentation]
Author = Daniel Devine
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Log over smtp (email).
diff --git a/nikola/plugins/loghandler/smtp.py b/nikola/plugins/loghandler/smtp.py
index 2c9fd9c..146a658 100644
--- a/nikola/plugins/loghandler/smtp.py
+++ b/nikola/plugins/loghandler/smtp.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Daniel Devine and others.
+# Copyright © 2012-2015 Daniel Devine and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/loghandler/stderr.plugin b/nikola/plugins/loghandler/stderr.plugin
index 211d2b4..6c20ea1 100644
--- a/nikola/plugins/loghandler/stderr.plugin
+++ b/nikola/plugins/loghandler/stderr.plugin
@@ -4,6 +4,6 @@ Module = stderr
[Documentation]
Author = Daniel Devine
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Log to stderr, the default logger.
diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py
index 593c381..79ace68 100644
--- a/nikola/plugins/loghandler/stderr.py
+++ b/nikola/plugins/loghandler/stderr.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Daniel Devine and others.
+# Copyright © 2012-2015 Daniel Devine and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/misc/scan_posts.plugin b/nikola/plugins/misc/scan_posts.plugin
new file mode 100644
index 0000000..6d2351f
--- /dev/null
+++ b/nikola/plugins/misc/scan_posts.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = scan_posts
+Module = scan_posts
+
+[Documentation]
+Author = Roberto Alsina
+Version = 1.0
+Website = http://getnikola.com
+Description = Scan posts and create timeline
+
diff --git a/nikola/plugins/misc/scan_posts.py b/nikola/plugins/misc/scan_posts.py
new file mode 100644
index 0000000..a6f04e6
--- /dev/null
+++ b/nikola/plugins/misc/scan_posts.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2015 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals, print_function
+import glob
+import os
+import sys
+
+from nikola.plugin_categories import PostScanner
+from nikola import utils
+from nikola.post import Post
+
+
+class ScanPosts(PostScanner):
+ """Render pages into output."""
+
+ name = "scan_posts"
+
+ def scan(self):
+ """Create list of posts from POSTS and PAGES options."""
+
+ seen = set([])
+ if not self.site.quiet:
+ print("Scanning posts", end='', file=sys.stderr)
+
+ timeline = []
+
+ for wildcard, destination, template_name, use_in_feeds in \
+ self.site.config['post_pages']:
+ if not self.site.quiet:
+ print(".", end='', file=sys.stderr)
+ 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/
+ # Get all the untranslated paths
+ dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst
+ untranslated = glob.glob(dir_glob)
+ # And now get all the translated paths
+ translated = set([])
+ for lang in self.site.config['TRANSLATIONS'].keys():
+ if lang == self.site.config['DEFAULT_LANG']:
+ continue
+ lang_glob = utils.get_translation_candidate(self.site.config, dir_glob, lang) # posts/foo/*.LANG.rst
+ translated = translated.union(set(glob.glob(lang_glob)))
+ # untranslated globs like *.rst often match translated paths too, so remove them
+ # and ensure x.rst is not in the translated set
+ untranslated = set(untranslated) - translated
+
+ # also remove from translated paths that are translations of
+ # paths in untranslated_list, so x.es.rst is not in the untranslated set
+ for p in untranslated:
+ translated = translated - set([utils.get_translation_candidate(self.site.config, p, l) for l in self.site.config['TRANSLATIONS'].keys()])
+
+ full_list = list(translated) + list(untranslated)
+ # We eliminate from the list the files inside any .ipynb folder
+ full_list = [p for p in full_list
+ if not any([x.startswith('.')
+ for x in p.split(os.sep)])]
+
+ for base_path in full_list:
+ if base_path in seen:
+ continue
+ else:
+ seen.add(base_path)
+ post = Post(
+ base_path,
+ self.site.config,
+ dest_dir,
+ use_in_feeds,
+ self.site.MESSAGES,
+ template_name,
+ self.site.get_compiler(base_path)
+ )
+ timeline.append(post)
+
+ return timeline
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/task/__init__.py
+++ b/nikola/plugins/task/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 448b115..6687209 100644
--- a/nikola/plugins/task/archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -4,7 +4,7 @@ Module = archive
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generates the blog's archive pages.
diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py
index 4f1ab19..533be69 100644
--- a/nikola/plugins/task/archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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,14 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+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
+from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name_for_index_link
class Archive(Task):
@@ -39,133 +41,191 @@ class Archive(Task):
def set_site(self, 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):
+ # name: used to build permalink and destination
+ # posts, items: posts or items; only one of them should be used,
+ # the other 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
+
+ context = {}
+ context["lang"] = lang
+ context["title"] = title
+ context["permalink"] = self.site.link("archive", name, lang)
+ if posts is not None:
+ context["posts"] = posts
+ n = len(posts)
+ else:
+ context["items"] = items
+ n = len(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 = {1: copy.copy(kw), 2: n}
+ 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):
+ 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')]
+ yield self.site.generic_index_renderer(
+ lang,
+ posts,
+ title,
+ "archiveindex.tmpl",
+ {"archive_name": name,
+ "is_feed_stale": kw["is_feed_stale"]},
+ kw,
+ str(self.name),
+ page_link,
+ page_path,
+ uptodate)
+ else:
+ yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable)
+
def gen_tasks(self):
kw = {
"messages": self.site.MESSAGES,
"translations": self.site.config['TRANSLATIONS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "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']:
+ 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"]:
- archdata = self.site.posts_per_year
- # A bit of a hack.
- if kw['create_single_archive']:
- archdata = {None: self.site.posts}
+ 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():
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", year, lang))
- context = {}
- context["lang"] = lang
+ # 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:
- context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ title = kw["messages"][lang]["Posts for year %s"] % year
+ kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != year)
else:
- context["title"] = kw["messages"][lang]["Archive"]
- context["permalink"] = self.site.link("archive", year, lang)
- if not kw["create_monthly_archive"]:
- template_name = "list_post.tmpl"
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- context["posts"] = post_list
- else: # Monthly archives, just list the months
- months = set([(m.split('/')[1], self.site.link("archive", m, lang)) for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
- months = sorted(list(months))
- months.reverse()
- template_name = "list.tmpl"
- context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link] for month, link in months]
- post_list = []
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- n = len(post_list) if 'posts' in context else len(months)
-
+ 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)) 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] for month, link in months]
+ yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable)
- task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: n, 4: deps_translatable}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- if not kw["create_monthly_archive"]:
+ 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
- template_name = "list_post.tmpl"
for yearmonth, posts in self.site.posts_per_month.items():
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", yearmonth,
- lang))
+ # Add archive per month
year, month = yearmonth.split('/')
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- context = {}
- context["lang"] = lang
- context["posts"] = post_list
- context["permalink"] = self.site.link("archive", year, lang)
-
- context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
- year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: len(post_list)}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- if not kw['create_single_archive']:
+
+ 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)
- template_name = "list.tmpl"
kw['years'] = years
for lang in kw["translations"]:
- context = {}
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", None,
- lang))
- context["title"] = kw["messages"][lang]["Archive"]
- context["items"] = [(y, self.site.link("archive", y, lang))
- for y in years]
- context["permalink"] = self.site.link("archive", None, lang)
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw, 3: len(years)}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
- def archive_path(self, name, lang):
+ items = [(y, self.site.link("archive", y, lang)) 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):
+ 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,
- self.site.config['INDEX_FILE']] if _f]
+ index_file] if _f]
else:
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
self.site.config['ARCHIVE_PATH'],
- self.site.config['ARCHIVE_FILENAME']] if _f]
+ archive_file] if _f]
+
+ def archive_atom_path(self, name, lang):
+ return self.archive_path(name, lang, is_feed=True)
diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin
index e0b0a4d..3fe049b 100644
--- a/nikola/plugins/task/bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -4,7 +4,7 @@ Module = bundles
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Theme bundles using WebAssets
diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py
index fca6924..6f88d0c 100644
--- a/nikola/plugins/task/bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -43,11 +43,12 @@ class BuildBundles(LateTask):
name = "create_bundles"
def set_site(self, site):
- super(BuildBundles, self).set_site(site)
- if webassets is None and self.site.config['USE_BUNDLES']:
+ self.logger = utils.get_logger('bundles', site.loghandlers)
+ if webassets is None and site.config['USE_BUNDLES']:
utils.req_missing(['webassets'], 'USE_BUNDLES', optional=True)
- utils.LOGGER.warn('Setting USE_BUNDLES to False.')
- self.site.config['USE_BUNDLES'] = False
+ self.logger.warn('Setting USE_BUNDLES to False.')
+ site.config['USE_BUNDLES'] = False
+ super(BuildBundles, self).set_site(site)
def gen_tasks(self):
"""Bundle assets using WebAssets."""
@@ -74,7 +75,12 @@ class BuildBundles(LateTask):
bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
env.register(output, bundle)
# This generates the file
- env[output].urls()
+ 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
@@ -91,8 +97,7 @@ class BuildBundles(LateTask):
files.append(os.path.join(dname, fname))
file_dep = [os.path.join(kw['output_folder'], fname)
for fname in files if
- utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS'])
- or fname == 'assets/css/code.css']
+ utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS']) or fname == os.path.join('assets', 'css', 'code.css')]
# code.css will be generated by us if it does not exist in
# FILES_FOLDERS or theme assets. It is guaranteed that the
# generation will happen before this task.
@@ -107,7 +112,7 @@ class BuildBundles(LateTask):
utils.config_changed({
1: kw,
2: file_dep
- })],
+ }, 'nikola.plugins.task.bundles')],
'clean': True,
}
yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index 28b9e32..0530ebf 100644
--- a/nikola/plugins/task/copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -4,7 +4,7 @@ Module = copy_assets
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Copy theme assets into output.
diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py
index 29aa083..a72bfdf 100644
--- a/nikola/plugins/task/copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -72,7 +72,7 @@ class CopyAssets(Task):
if task['name'] in tasks:
continue
tasks[task['name']] = task
- task['uptodate'] = [utils.config_changed(kw)]
+ task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')]
task['basename'] = self.name
if code_css_input:
task['file_dep'] = [code_css_input]
@@ -99,7 +99,7 @@ class CopyAssets(Task):
'basename': self.name,
'name': code_css_path,
'targets': [code_css_path],
- 'uptodate': [utils.config_changed(kw), testcontents],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents],
'actions': [(create_code_css, [])],
'clean': True,
}
diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index 45c9e0d..073676b 100644
--- a/nikola/plugins/task/copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -4,7 +4,7 @@ Module = copy_files
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Copy static files into the output.
diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py
index 1d31756..9a039f1 100644
--- a/nikola/plugins/task/copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,5 +51,5 @@ class CopyFiles(Task):
real_dst = os.path.join(dst, kw['files_folders'][src])
for task in utils.copy_tree(src, real_dst, link_cutoff=dst):
task['basename'] = self.name
- task['uptodate'] = [utils.config_changed(kw)]
+ task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_files')]
yield utils.apply_filters(task, filters, skip_ext=['.html'])
diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin
index 8352151..73085cd 100644
--- a/nikola/plugins/task/galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -4,7 +4,7 @@ Module = galleries
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create image galleries automatically.
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
index f835444..e887f18 100644
--- a/nikola/plugins/task/galleries.py
+++ b/nikola/plugins/task/galleries.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,34 +31,30 @@ import glob
import json
import mimetypes
import os
+import sys
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoin # NOQA
import natsort
-Image = None
try:
- from PIL import Image, ExifTags # NOQA
+ from PIL import Image # NOQA
except ImportError:
- try:
- import Image as _Image
- import ExifTags
- Image = _Image
- except ImportError:
- pass
+ import Image as _Image
+ Image = _Image
import PyRSS2Gen as rss
from nikola.plugin_categories import Task
from nikola import utils
+from nikola.image_processing import ImageProcessor
from nikola.post import Post
-from nikola.utils import req_missing
_image_size_cache = {}
-class Galleries(Task):
+class Galleries(Task, ImageProcessor):
"""Render image galleries."""
name = 'render_galleries'
@@ -66,47 +62,84 @@ class Galleries(Task):
def set_site(self, site):
site.register_path_handler('gallery', self.gallery_path)
+ site.register_path_handler('gallery_global', self.gallery_global_path)
site.register_path_handler('gallery_rss', self.gallery_rss_path)
+
+ self.logger = utils.get_logger('render_galleries', site.loghandlers)
+
+ self.kw = {
+ 'thumbnail_size': site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': site.config['OUTPUT_FOLDER'],
+ 'cache_folder': site.config['CACHE_FOLDER'],
+ 'default_lang': site.config['DEFAULT_LANG'],
+ 'use_filename_as_title': site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_folders': site.config['GALLERY_FOLDERS'],
+ 'sort_by_date': site.config['GALLERY_SORT_BY_DATE'],
+ 'filters': site.config['FILTERS'],
+ 'translations': site.config['TRANSLATIONS'],
+ 'global_context': site.GLOBAL_CONTEXT,
+ 'feed_length': site.config['FEED_LENGTH'],
+ 'tzinfo': site.tzinfo,
+ 'comments_in_galleries': site.config['COMMENTS_IN_GALLERIES'],
+ 'generate_rss': site.config['GENERATE_RSS'],
+ }
+
+ # Verify that no folder in GALLERY_FOLDERS appears twice
+ appearing_paths = set()
+ for source, dest in self.kw['gallery_folders'].items():
+ if source in appearing_paths or dest in appearing_paths:
+ problem = source if source in appearing_paths else dest
+ utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, exiting.".format(problem))
+ sys.exit(1)
+ appearing_paths.add(source)
+ appearing_paths.add(dest)
+
+ # Find all galleries we need to process
+ self.find_galleries()
+ # 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.
+ if name in self.proper_gallery_links:
+ return self.proper_gallery_links[name]
+ elif name in self.improper_gallery_links:
+ candidates = self.improper_gallery_links[name]
+ if len(candidates) == 1:
+ return candidates[0]
+ self.logger.error("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates))
+ else:
+ self.logger.error("Unknown gallery '{0}'!".format(name))
+ self.logger.info("Known galleries: " + str(list(self.proper_gallery_links.keys())))
+ sys.exit(1)
+
def gallery_path(self, name, lang):
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['GALLERY_PATH'], name,
- self.site.config['INDEX_FILE']] if _f]
+ gallery_path = self._find_gallery_path(name)
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] +
+ gallery_path.split(os.sep) +
+ [self.site.config['INDEX_FILE']] if _f]
+
+ def gallery_global_path(self, name, lang):
+ gallery_path = self._find_gallery_path(name)
+ return [_f for _f in gallery_path.split(os.sep) +
+ [self.site.config['INDEX_FILE']] if _f]
def gallery_rss_path(self, name, lang):
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['GALLERY_PATH'], name,
- 'rss.xml'] if _f]
+ 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]
def gen_tasks(self):
"""Render image galleries."""
- if Image is None:
- req_missing(['pillow'], 'render galleries')
-
- self.logger = utils.get_logger('render_galleries', self.site.loghandlers)
- self.image_ext_list = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+ self.image_ext_list = self.image_ext_list_builtin
self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
- self.kw = {
- 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
- 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
- 'output_folder': self.site.config['OUTPUT_FOLDER'],
- 'cache_folder': self.site.config['CACHE_FOLDER'],
- 'default_lang': self.site.config['DEFAULT_LANG'],
- 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
- 'gallery_path': self.site.config['GALLERY_PATH'],
- 'sort_by_date': self.site.config['GALLERY_SORT_BY_DATE'],
- 'filters': self.site.config['FILTERS'],
- 'translations': self.site.config['TRANSLATIONS'],
- 'global_context': self.site.GLOBAL_CONTEXT,
- 'feed_length': self.site.config['FEED_LENGTH'],
- 'tzinfo': self.site.tzinfo,
- 'comments_in_galleries': self.site.config['COMMENTS_IN_GALLERIES'],
- 'generate_rss': self.site.config['GENERATE_RSS'],
- }
-
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
self.kw['||template_hooks|{0}||'.format(k)] = v._items
@@ -114,22 +147,19 @@ class Galleries(Task):
template_name = "gallery.tmpl"
- # Find all galleries we need to process
- self.find_galleries()
-
# Create all output folders
for task in self.create_galleries():
yield task
# For each gallery:
- for gallery in self.gallery_list:
+ for gallery, input_folder, output_folder in self.gallery_list:
# Create subfolder list
folder_list = [(x, x.split(os.sep)[-2]) for x in
glob.glob(os.path.join(gallery, '*') + os.sep)]
# Parse index into a post (with translations)
- post = self.parse_index(gallery)
+ post = self.parse_index(gallery, input_folder, output_folder)
# Create image list, filter exclusions
image_list = self.get_image_list(gallery)
@@ -143,12 +173,12 @@ class Galleries(Task):
# Create thumbnails and large images in destination
for image in image_list:
- for task in self.create_target_images(image):
+ for task in self.create_target_images(image, input_folder):
yield task
# Remove excluded images
for image in self.get_excluded_images(gallery):
- for task in self.remove_excluded_image(image):
+ for task in self.remove_excluded_image(image, input_folder):
yield task
crumbs = utils.get_crumbs(gallery, index_folder=self)
@@ -160,9 +190,7 @@ class Galleries(Task):
dst = os.path.join(
self.kw['output_folder'],
- self.site.path(
- "gallery",
- os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ self.site.path("gallery", gallery, lang))
dst = os.path.normpath(dst)
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
@@ -187,25 +215,27 @@ class Galleries(Task):
img_titles = [''] * len(image_name_list)
thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list]
- thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs]
- dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in image_list]
+ thumbs = [os.path.join(self.kw['output_folder'], output_folder, os.path.relpath(t, input_folder)) for t in thumbs]
+ dst_img_list = [os.path.join(output_folder, os.path.relpath(t, input_folder)) for t in image_list]
+ dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in dst_img_list]
folders = []
# Generate friendly gallery names
for path, folder in folder_list:
- fpost = self.parse_index(path)
+ fpost = self.parse_index(path, input_folder, output_folder)
if fpost:
ft = fpost.title(lang) or folder
else:
ft = folder
+ if not folder.endswith('/'):
+ folder += '/'
folders.append((folder, ft))
- context["folders"] = natsort.natsorted(folders)
+ context["folders"] = natsort.natsorted(
+ folders, alg=natsort.ns.F | natsort.ns.IC)
context["crumbs"] = crumbs
- context["permalink"] = self.site.link(
- "gallery", os.path.basename(
- os.path.relpath(gallery, self.kw['gallery_path'])), lang)
+ context["permalink"] = self.site.link("gallery", gallery, lang)
context["enable_comments"] = self.kw['comments_in_galleries']
context["thumbnail_size"] = self.kw["thumbnail_size"]
@@ -216,15 +246,18 @@ class Galleries(Task):
'targets': [post.translated_base_path(lang)],
'file_dep': post.fragment_deps(lang),
'actions': [(post.compile, [lang])],
- 'uptodate': [utils.config_changed(self.kw)]
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:post')] + post.fragment_deps_uptodate(lang)
}
context['post'] = post
else:
context['post'] = None
file_dep = self.site.template_system.template_deps(
template_name) + image_list + thumbs
+ file_dep_dest = self.site.template_system.template_deps(
+ template_name) + dest_img_list + thumbs
if post:
file_dep += [post.translated_base_path(l) for l in self.kw['translations']]
+ file_dep_dest += [post.translated_base_path(l) for l in self.kw['translations']]
yield utils.apply_filters({
'basename': self.name,
@@ -244,58 +277,87 @@ class Galleries(Task):
'uptodate': [utils.config_changed({
1: self.kw,
2: self.site.config["COMMENTS_IN_GALLERIES"],
- 3: context,
- })],
+ 3: context.copy(),
+ }, 'nikola.plugins.task.galleries:gallery')],
}, self.kw['filters'])
# RSS for the gallery
if self.kw["generate_rss"]:
rss_dst = os.path.join(
self.kw['output_folder'],
- self.site.path(
- "gallery_rss",
- os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ self.site.path("gallery_rss", gallery, lang))
rss_dst = os.path.normpath(rss_dst)
yield utils.apply_filters({
'basename': self.name,
'name': rss_dst,
- 'file_dep': file_dep,
+ 'file_dep': file_dep_dest,
'targets': [rss_dst],
'actions': [
(self.gallery_rss, (
image_list,
+ dst_img_list,
img_titles,
lang,
- self.site.link(
- "gallery_rss", os.path.basename(gallery), lang),
+ self.site.link("gallery_rss", gallery, lang),
rss_dst,
context['title']
))],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw,
- })],
+ }, 'nikola.plugins.task.galleries:rss')],
}, self.kw['filters'])
def find_galleries(self):
"""Find all galleries to be processed according to conf.py"""
self.gallery_list = []
- for root, dirs, files in os.walk(self.kw['gallery_path'], followlinks=True):
- self.gallery_list.append(root)
+ for input_folder, output_folder in self.kw['gallery_folders'].items():
+ for root, dirs, files in os.walk(input_folder, followlinks=True):
+ self.gallery_list.append((root, input_folder, output_folder))
+
+ def create_galleries_paths(self):
+ """Given a list of galleries, puts their paths into self.gallery_links."""
+
+ # gallery_path is "gallery/foo/name"
+ self.proper_gallery_links = dict()
+ self.improper_gallery_links = dict()
+ for gallery_path, input_folder, output_folder in self.gallery_list:
+ if gallery_path == input_folder:
+ gallery_name = ''
+ # special case, because relpath will return '.' in this case
+ else:
+ gallery_name = os.path.relpath(gallery_path, input_folder)
+
+ output_path = os.path.join(output_folder, gallery_name)
+ self.proper_gallery_links[gallery_path] = output_path
+ self.proper_gallery_links[output_path] = output_path
+
+ # If the input and output names differ, the gallery is accessible
+ # only by `input` and `output/`.
+ output_path_noslash = output_path[:-1]
+ if output_path_noslash not in self.proper_gallery_links:
+ self.proper_gallery_links[output_path_noslash] = output_path
+
+ gallery_path_slash = gallery_path + '/'
+ if gallery_path_slash not in self.proper_gallery_links:
+ self.proper_gallery_links[gallery_path_slash] = output_path
+
+ if gallery_name not in self.improper_gallery_links:
+ self.improper_gallery_links[gallery_name] = list()
+ self.improper_gallery_links[gallery_name].append(output_path)
def create_galleries(self):
"""Given a list of galleries, create the output folders."""
# gallery_path is "gallery/foo/name"
- for gallery_path in self.gallery_list:
- gallery_name = os.path.relpath(gallery_path, self.kw['gallery_path'])
+ for gallery_path, input_folder, _ in self.gallery_list:
# have to use dirname because site.path returns .../index.html
output_gallery = os.path.dirname(
os.path.join(
self.kw["output_folder"],
- self.site.path("gallery", gallery_name)))
+ self.site.path("gallery", gallery_path)))
output_gallery = os.path.normpath(output_gallery)
# Task to create gallery in output/
yield {
@@ -304,16 +366,16 @@ class Galleries(Task):
'actions': [(utils.makedirs, (output_gallery,))],
'targets': [output_gallery],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:mkdir')],
}
- def parse_index(self, gallery):
+ def parse_index(self, gallery, input_folder, output_folder):
"""Returns a Post object if there is an index.txt."""
index_path = os.path.join(gallery, "index.txt")
destination = os.path.join(
- self.kw["output_folder"],
- gallery)
+ self.kw["output_folder"], output_folder,
+ os.path.relpath(gallery, input_folder))
if os.path.isfile(index_path):
post = Post(
index_path,
@@ -361,12 +423,12 @@ class Galleries(Task):
image_list = list(image_set)
return image_list
- def create_target_images(self, img):
- gallery_name = os.path.relpath(os.path.dirname(img), self.kw['gallery_path'])
+ def create_target_images(self, img, input_path):
+ gallery_name = os.path.dirname(img)
output_gallery = os.path.dirname(
os.path.join(
self.kw["output_folder"],
- self.site.path("gallery", gallery_name)))
+ self.site.path("gallery_global", gallery_name)))
# Do thumbnails and copy originals
# img is "galleries/name/image_name.jpg"
# img_name is "image_name.jpg"
@@ -392,7 +454,7 @@ class Galleries(Task):
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['thumbnail_size']
- })],
+ }, 'nikola.plugins.task.galleries:resize_thumb')],
}, self.kw['filters'])
yield utils.apply_filters({
@@ -407,19 +469,19 @@ class Galleries(Task):
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['max_image_size']
- })],
+ }, 'nikola.plugins.task.galleries:resize_max')],
}, self.kw['filters'])
- def remove_excluded_image(self, img):
+ def remove_excluded_image(self, img, input_folder):
# Remove excluded images
- # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path
+ # img is something like input_folder/demo/tesla2_lg.jpg so it's the *source* path
# and we should remove both the large and thumbnail *destination* paths
- img = os.path.relpath(img, self.kw['gallery_path'])
output_folder = os.path.dirname(
os.path.join(
self.kw["output_folder"],
- self.site.path("gallery", os.path.dirname(img))))
+ self.site.path("gallery_global", os.path.dirname(img))))
+ img = os.path.relpath(img, input_folder)
img_path = os.path.join(output_folder, os.path.basename(img))
fname, ext = os.path.splitext(img_path)
thumb_path = fname + '.thumbnail' + ext
@@ -431,7 +493,7 @@ class Galleries(Task):
(utils.remove_file, (thumb_path,))
],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_thumb')],
}, self.kw['filters'])
yield utils.apply_filters({
@@ -441,7 +503,7 @@ class Galleries(Task):
(utils.remove_file, (img_path,))
],
'clean': True,
- 'uptodate': [utils.config_changed(self.kw)],
+ 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_file')],
}, self.kw['filters'])
def render_gallery_index(
@@ -484,7 +546,7 @@ class Galleries(Task):
context['photo_array_json'] = json.dumps(photo_array)
self.site.render_template(template_name, output_name, context)
- def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title):
+ def gallery_rss(self, img_list, dest_img_list, img_titles, lang, permalink, output_path, title):
"""Create a RSS showing the latest images in the gallery.
This doesn't use generic_rss_renderer because it
@@ -492,10 +554,10 @@ class Galleries(Task):
"""
def make_url(url):
- return urljoin(self.site.config['BASE_URL'], url)
+ return urljoin(self.site.config['BASE_URL'], url.lstrip('/'))
items = []
- for img, title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]:
+ for img, srcimg, title in list(zip(dest_img_list, img_list, img_titles))[:self.kw["feed_length"]]:
img_size = os.stat(
os.path.join(
self.site.config['OUTPUT_FOLDER'], img)).st_size
@@ -503,7 +565,7 @@ class Galleries(Task):
'title': title,
'link': make_url(img),
'guid': rss.Guid(img, False),
- 'pubDate': self.image_date(img),
+ 'pubDate': self.image_date(srcimg),
'enclosure': rss.Enclosure(
make_url(img),
img_size,
@@ -515,12 +577,15 @@ class Galleries(Task):
title=title,
link=make_url(permalink),
description='',
- lastBuildDate=datetime.datetime.now(),
+ lastBuildDate=datetime.datetime.utcnow(),
items=items,
generator='http://getnikola.com/',
language=lang
)
+
rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
+ rss_obj.self_url = make_url(permalink)
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
dst_dir = os.path.dirname(output_path)
utils.makedirs(dst_dir)
with io.open(output_path, "w+", encoding="utf-8") as rss_file:
@@ -528,66 +593,3 @@ class Galleries(Task):
if isinstance(data, utils.bytes_str):
data = data.decode('utf-8')
rss_file.write(data)
-
- def resize_image(self, src, dst, max_size):
- """Make a copy of the image in the requested size."""
- if not Image:
- utils.copy_file(src, dst)
- return
- im = Image.open(src)
- w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
-
- # Panoramas get larger thumbnails because they look *awful*
- if w > 2 * h:
- size = min(w, max_size * 4), min(w, max_size * 4)
-
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
-
- if decoded == 'Orientation':
- if value == 3:
- im = im.rotate(180)
- elif value == 6:
- im = im.rotate(270)
- elif value == 8:
- im = im.rotate(90)
- break
- try:
- im.thumbnail(size, Image.ANTIALIAS)
- im.save(dst)
- except Exception as e:
- self.logger.warn("Can't thumbnail {0}, using original "
- "image as thumbnail ({1})".format(src, e))
- utils.copy_file(src, dst)
- else: # Image is small
- utils.copy_file(src, dst)
-
- def image_date(self, src):
- """Try to figure out the date of the image."""
- if src not in self.dates:
- try:
- im = Image.open(src)
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
- if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):
- try:
- self.dates[src] = datetime.datetime.strptime(
- value, r'%Y:%m:%d %H:%M:%S')
- break
- except ValueError: # Invalid EXIF date.
- pass
- if src not in self.dates:
- self.dates[src] = datetime.datetime.fromtimestamp(
- os.stat(src).st_mtime)
- return self.dates[src]
diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin
index b68ea6f..4867fd6 100644
--- a/nikola/plugins/task/gzip.plugin
+++ b/nikola/plugins/task/gzip.plugin
@@ -4,7 +4,7 @@ Module = gzip
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create gzipped copies of files
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
index bcc9637..5799839 100644
--- a/nikola/plugins/task/gzip.py
+++ b/nikola/plugins/task/gzip.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 a18942c..5d2bf5a 100644
--- a/nikola/plugins/task/indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -4,7 +4,7 @@ Module = indexes
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generates the blog's index pages.
diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py
index 0a2cd02..03d36b1 100644
--- a/nikola/plugins/task/indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 collections import defaultdict
import os
from nikola.plugin_categories import Task
-from nikola.utils import config_changed
+from nikola import utils
class Indexes(Task):
@@ -39,6 +39,7 @@ class Indexes(Task):
def set_site(self, site):
site.register_path_handler('index', self.index_path)
+ site.register_path_handler('index_atom', self.index_atom_path)
return super(Indexes, self).set_site(site)
def gen_tasks(self):
@@ -47,85 +48,39 @@ class Indexes(Task):
kw = {
"translations": self.site.config['TRANSLATIONS'],
- "index_display_post_count":
- self.site.config['INDEX_DISPLAY_POST_COUNT'],
"messages": self.site.MESSAGES,
- "index_teasers": self.site.config['INDEX_TEASERS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"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'],
- "indexes_pages": self.site.config['INDEXES_PAGES'],
- "indexes_pages_main": self.site.config['INDEXES_PAGES_MAIN'],
"blog_title": self.site.config["BLOG_TITLE"],
- "rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"],
+ "generate_atom": self.site.config["GENERATE_ATOM"],
}
template_name = "index.tmpl"
posts = self.site.posts
+ self.number_of_pages = dict()
for lang in kw["translations"]:
- # Split in smaller lists
- lists = []
+ 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)
+
if kw["show_untranslated_posts"]:
filtered_posts = posts
else:
filtered_posts = [x for x in posts if x.is_translation_available(lang)]
- lists.append(filtered_posts[:kw["index_display_post_count"]])
- filtered_posts = filtered_posts[kw["index_display_post_count"]:]
- while filtered_posts:
- lists.append(filtered_posts[-kw["index_display_post_count"]:])
- filtered_posts = filtered_posts[:-kw["index_display_post_count"]]
- num_pages = len(lists)
- for i, post_list in enumerate(lists):
- context = {}
- indexes_title = kw['indexes_title'] or kw['blog_title'](lang)
- if kw["indexes_pages_main"]:
- ipages_i = i + 1
- ipages_msg = "page %d"
- else:
- ipages_i = i
- ipages_msg = "old posts, page %d"
- if kw["indexes_pages"]:
- indexes_pages = kw["indexes_pages"] % ipages_i
- else:
- indexes_pages = " (" + \
- kw["messages"][lang][ipages_msg] % ipages_i + ")"
- if i > 0 or kw["indexes_pages_main"]:
- context["title"] = indexes_title + indexes_pages
- else:
- context["title"] = indexes_title
- context["prevlink"] = None
- context["nextlink"] = None
- context['index_teasers'] = kw['index_teasers']
- if i == 0: # index.html page
- context["prevlink"] = None
- if num_pages > 1:
- context["nextlink"] = "index-{0}.html".format(num_pages - 1)
- else:
- context["nextlink"] = None
- else: # index-x.html pages
- if i > 1:
- context["nextlink"] = "index-{0}.html".format(i - 1)
- if i < num_pages - 1:
- context["prevlink"] = "index-{0}.html".format(i + 1)
- elif i == num_pages - 1:
- context["prevlink"] = "index.html"
- context["permalink"] = self.site.link("index", i, lang)
- output_name = os.path.join(
- kw['output_folder'], self.site.path("index", i,
- lang))
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = 'render_indexes'
- yield task
+
+ indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang)
+ self.number_of_pages[lang] = (len(filtered_posts) + kw['index_display_post_count'] - 1) // kw['index_display_post_count']
+
+ yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, {}, kw, 'render_indexes', page_link, page_path)
if not self.site.config["STORY_INDEX"]:
return
@@ -135,6 +90,7 @@ class Indexes(Task):
"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'],
}
template_name = "list.tmpl"
for lang in kw["translations"]:
@@ -151,6 +107,12 @@ class Indexes(Task):
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('\\', '/')
+ index_len = len(kw['index_file'])
+ if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
+ link = link[:-index_len]
+ context["permalink"] = link
+
for post in post_list:
# If there is an index.html pending to be created from
# a story, do not generate the STORY_INDEX
@@ -166,18 +128,25 @@ class Indexes(Task):
template_name,
kw['filters'],
context)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
task['basename'] = self.name
yield task
- def index_path(self, name, lang):
- if name not in [None, 0]:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['INDEX_PATH'],
- 'index-{0}.html'.format(name)] if _f]
+ def index_path(self, name, lang, is_feed=False):
+ extension = None
+ if is_feed:
+ extension = ".atom"
+ index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['INDEX_PATH'],
- self.site.config['INDEX_FILE']]
- if _f]
+ index_file = self.site.config['INDEX_FILE']
+ 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, self.number_of_pages[lang], self.site),
+ lang,
+ self.site,
+ extension=extension)
+
+ def index_atom_path(self, name, lang):
+ return self.index_path(name, lang, is_feed=True)
diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin
index c93184d..a5ba77a 100644
--- a/nikola/plugins/task/listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -4,7 +4,7 @@ Module = listings
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Render code listings into output
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
index 79f6763..b913330 100644
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,74 +26,115 @@
from __future__ import unicode_literals, print_function
+import sys
import os
from pygments import highlight
from pygments.lexers import get_lexer_for_filename, TextLexer
-from pygments.formatters import HtmlFormatter
import natsort
-import re
from nikola.plugin_categories import Task
from nikola import utils
-# FIXME: (almost) duplicated with mdx_nikola.py
-CODERE = re.compile('<div class="code"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL)
-
-
class Listings(Task):
"""Render pretty listings."""
name = "render_listings"
+ def register_output_name(self, input_folder, rel_name, rel_output_name):
+ """Register proper and improper file mappings."""
+ if rel_name not in self.improper_input_file_mapping:
+ self.improper_input_file_mapping[rel_name] = []
+ self.improper_input_file_mapping[rel_name].append(rel_output_name)
+ self.proper_input_file_mapping[os.path.join(input_folder, rel_name)] = rel_output_name
+ self.proper_input_file_mapping[rel_output_name] = rel_output_name
+
def set_site(self, site):
site.register_path_handler('listing', self.listing_path)
+
+ # We need to prepare some things for the listings path handler to work.
+
+ self.kw = {
+ "default_lang": site.config["DEFAULT_LANG"],
+ "listings_folders": site.config["LISTINGS_FOLDERS"],
+ "output_folder": site.config["OUTPUT_FOLDER"],
+ "index_file": site.config["INDEX_FILE"],
+ "strip_indexes": site.config['STRIP_INDEXES'],
+ "filters": site.config["FILTERS"],
+ }
+
+ # Verify that no folder in LISTINGS_FOLDERS appears twice (on output side)
+ appearing_paths = set()
+ for source, dest in self.kw['listings_folders'].items():
+ if source in appearing_paths or dest in appearing_paths:
+ problem = source if source in appearing_paths else dest
+ utils.LOGGER.error("The listings input or output folder '{0}' appears in more than one entry in LISTINGS_FOLDERS, exiting.".format(problem))
+ sys.exit(1)
+ appearing_paths.add(source)
+ appearing_paths.add(dest)
+
+ # improper_input_file_mapping maps a relative input file (relative to
+ # its corresponding input directory) to a list of the output files.
+ # Since several input directories can contain files of the same name,
+ # a list is needed. This is needed for compatibility to previous Nikola
+ # versions, where there was no need to specify the input directory name
+ # when asking for a link via site.link('listing', ...).
+ self.improper_input_file_mapping = {}
+
+ # proper_input_file_mapping maps relative input file (relative to CWD)
+ # to a generated output file. Since we don't allow an input directory
+ # to appear more than once in LISTINGS_FOLDERS, we can map directly to
+ # a file name (and not a list of files).
+ 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):
+ # 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:
+ rel_path = rel_path[1:]
+
+ for f in files + [self.kw['index_file']]:
+ rel_name = os.path.join(rel_path, f)
+ rel_output_name = os.path.join(output_folder, rel_path, f)
+ # Register file names in the mapping.
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+
return super(Listings, self).set_site(site)
def gen_tasks(self):
"""Render pretty code listings."""
- kw = {
- "default_lang": self.site.config["DEFAULT_LANG"],
- "listings_folder": self.site.config["LISTINGS_FOLDER"],
- "output_folder": self.site.config["OUTPUT_FOLDER"],
- "index_file": self.site.config["INDEX_FILE"],
- }
# Things to ignore in listings
ignored_extensions = (".pyc", ".pyo")
- def render_listing(in_name, out_name, folders=[], files=[]):
+ def render_listing(in_name, out_name, input_folder, output_folder, folders=[], files=[]):
if in_name:
with open(in_name, 'r') as fd:
try:
lexer = get_lexer_for_filename(in_name)
except:
lexer = TextLexer()
- code = highlight(fd.read(), lexer,
- HtmlFormatter(cssclass='code',
- linenos="table", nowrap=False,
- lineanchors=utils.slugify(in_name, force=True),
- anchorlinenos=True))
- # the pygments highlighter uses <div class="codehilite"><pre>
- # for code. We switch it to reST's <pre class="code">.
- code = CODERE.sub('<pre class="code literal-block">\\1</pre>', code)
+ code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name))
title = os.path.basename(in_name)
else:
code = ''
- title = ''
+ title = os.path.split(os.path.dirname(out_name))[1]
crumbs = utils.get_crumbs(os.path.relpath(out_name,
- kw['output_folder']),
+ self.kw['output_folder']),
is_file=True)
permalink = self.site.link(
'listing',
- os.path.relpath(
- out_name,
- os.path.join(
- kw['output_folder'],
- kw['listings_folder'])))
- if self.site.config['COPY_SOURCES']:
- source_link = permalink[:-5]
+ os.path.join(
+ input_folder,
+ os.path.relpath(
+ out_name[:-5], # remove '.html'
+ os.path.join(
+ self.kw['output_folder'],
+ output_folder))))
+ if self.site.config['COPY_SOURCES'] and in_name:
+ source_link = permalink[:-5] # remove '.html'
else:
source_link = None
context = {
@@ -101,88 +142,121 @@ class Listings(Task):
'title': title,
'crumbs': crumbs,
'permalink': permalink,
- 'lang': kw['default_lang'],
- 'folders': natsort.natsorted(folders),
- 'files': natsort.natsorted(files),
+ 'lang': self.kw['default_lang'],
+ 'folders': natsort.natsorted(
+ folders, alg=natsort.ns.F | natsort.ns.IC),
+ 'files': natsort.natsorted(
+ files, alg=natsort.ns.F | natsort.ns.IC),
'description': title,
'source_link': source_link,
}
- self.site.render_template('listing.tmpl', out_name,
- context)
+ self.site.render_template('listing.tmpl', out_name, context)
yield self.group_task()
template_deps = self.site.template_system.template_deps('listing.tmpl')
- for root, dirs, files in os.walk(kw['listings_folder'], followlinks=True):
- files = [f for f in files if os.path.splitext(f)[-1] not in ignored_extensions]
-
- uptodate = {'c': self.site.GLOBAL_CONTEXT}
-
- for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- uptodate['||template_hooks|{0}||'.format(k)] = v._items
-
- for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
- uptodate[k] = self.site.GLOBAL_CONTEXT[k](kw['default_lang'])
-
- # save navigation links as dependencies
- uptodate['navigation_links'] = uptodate['c']['navigation_links'](kw['default_lang'])
-
- uptodate2 = uptodate.copy()
- uptodate2['f'] = files
- uptodate2['d'] = dirs
-
- # Render all files
- out_name = os.path.join(
- kw['output_folder'],
- root, kw['index_file']
- )
- yield {
- 'basename': self.name,
- 'name': out_name,
- 'file_dep': template_deps,
- 'targets': [out_name],
- 'actions': [(render_listing, [None, out_name, dirs, files])],
- # This is necessary to reflect changes in blog title,
- # sidebar links, etc.
- 'uptodate': [utils.config_changed(uptodate2)],
- 'clean': True,
- }
- for f in files:
- ext = os.path.splitext(f)[-1]
- if ext in ignored_extensions:
- continue
- in_name = os.path.join(root, f)
- out_name = os.path.join(
- kw['output_folder'],
- root,
- f) + '.html'
- yield {
+
+ for input_folder, output_folder in self.kw['listings_folders'].items():
+ for root, dirs, files in os.walk(input_folder, followlinks=True):
+ files = [f for f in files if os.path.splitext(f)[-1] not in ignored_extensions]
+
+ uptodate = {'c': self.site.GLOBAL_CONTEXT}
+
+ for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
+ uptodate['||template_hooks|{0}||'.format(k)] = v._items
+
+ for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
+ uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang'])
+
+ # save navigation links as dependencies
+ uptodate['navigation_links'] = uptodate['c']['navigation_links'](self.kw['default_lang'])
+
+ uptodate['kw'] = self.kw
+
+ uptodate2 = uptodate.copy()
+ uptodate2['f'] = files
+ uptodate2['d'] = dirs
+
+ # 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:
+ rel_path = rel_path[1:]
+
+ rel_name = os.path.join(rel_path, self.kw['index_file'])
+ rel_output_name = os.path.join(output_folder, rel_path, self.kw['index_file'])
+
+ # Render all files
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ yield utils.apply_filters({
'basename': self.name,
'name': out_name,
- 'file_dep': template_deps + [in_name],
+ 'file_dep': template_deps,
'targets': [out_name],
- 'actions': [(render_listing, [in_name, out_name])],
+ 'actions': [(render_listing, [None, out_name, input_folder, output_folder, dirs, files])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
- 'uptodate': [utils.config_changed(uptodate)],
+ 'uptodate': [utils.config_changed(uptodate2, 'nikola.plugins.task.listings:folder')],
'clean': True,
- }
- if self.site.config['COPY_SOURCES']:
- out_name = os.path.join(
- kw['output_folder'],
- root,
- f)
- yield {
+ }, self.kw["filters"])
+ for f in files:
+ ext = os.path.splitext(f)[-1]
+ if ext in ignored_extensions:
+ continue
+ in_name = os.path.join(root, f)
+ # Record file names
+ rel_name = os.path.join(rel_path, f + '.html')
+ rel_output_name = os.path.join(output_folder, rel_path, f + '.html')
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+ # Set up output name
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ # Yield task
+ yield utils.apply_filters({
'basename': self.name,
'name': out_name,
- 'file_dep': [in_name],
+ 'file_dep': template_deps + [in_name],
'targets': [out_name],
- 'actions': [(utils.copy_file, [in_name, out_name])],
+ 'actions': [(render_listing, [in_name, out_name, input_folder, output_folder])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(uptodate, 'nikola.plugins.task.listings:source')],
'clean': True,
- }
+ }, self.kw["filters"])
+ if self.site.config['COPY_SOURCES']:
+ rel_name = os.path.join(rel_path, f)
+ rel_output_name = os.path.join(output_folder, rel_path, f)
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': out_name,
+ 'file_dep': [in_name],
+ 'targets': [out_name],
+ 'actions': [(utils.copy_file, [in_name, out_name])],
+ 'clean': True,
+ }, self.kw["filters"])
- def listing_path(self, name, lang):
- if not name.endswith('.html'):
+ def listing_path(self, namep, lang):
+ namep = namep.replace('/', os.sep)
+ nameh = namep + '.html'
+ for name in (namep, nameh):
+ if name in self.proper_input_file_mapping:
+ # If the name shows up in this dict, everything's fine.
+ name = self.proper_input_file_mapping[name]
+ break
+ elif name in self.improper_input_file_mapping:
+ # If the name shows up in this dict, we have to check for
+ # ambiguities.
+ if len(self.improper_input_file_mapping[name]) > 1:
+ 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])))
+ sys.exit(1)
+ 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.")
+ name = self.improper_input_file_mapping[name][0]
+ break
+ else:
+ utils.LOGGER.error("Unknown listing name {0}!".format(namep))
+ sys.exit(1)
+ if not name.endswith(os.sep + self.site.config["INDEX_FILE"]):
name += '.html'
- path_parts = [self.site.config['LISTINGS_FOLDER']] + list(os.path.split(name))
+ path_parts = name.split(os.sep)
return [_f for _f in path_parts if _f]
diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin
index 67212d2..4cad7b7 100644
--- a/nikola/plugins/task/pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -4,7 +4,7 @@ Module = pages
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create pages in the output.
diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py
index aefc5a1..d0edb56 100644
--- a/nikola/plugins/task/pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,9 +51,7 @@ class RenderPages(Task):
continue
for task in self.site.generic_page_renderer(lang, post,
kw["filters"]):
- task['uptodate'] = [config_changed({
- 1: task['uptodate'][0].config,
- 2: kw})]
+ task['uptodate'] = task['uptodate'] + [config_changed(kw, 'nikola.plugins.task.pages')]
task['basename'] = self.name
task['task_dep'] = ['render_posts']
yield task
diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin
index e1a42fd..707b3c2 100644
--- a/nikola/plugins/task/posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -4,7 +4,7 @@ Module = posts
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create HTML fragments out of posts.
diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py
index 8e03122..d3f17fd 100644
--- a/nikola/plugins/task/posts.py
+++ b/nikola/plugins/task/posts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -25,18 +25,20 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from copy import copy
+import os
from nikola.plugin_categories import Task
-from nikola import utils
+from nikola import filters, utils
-def rest_deps(post, task):
- """Add extra_deps from ReST into task.
+def update_deps(post, lang, task):
+ """Updates file dependencies as they might have been updated during compilation.
- The .dep file is created by ReST so not available before the task starts
- to execute.
+ This is done for example by the ReST page compiler, which writes its
+ dependencies into a .dep file. This file is read and incorporated when calling
+ post.fragment_deps(), and only available /after/ compiling the fragment.
"""
- task.file_dep.update(post.extra_deps())
+ task.file_dep.update([p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")])
class RenderPosts(Task):
@@ -54,23 +56,62 @@ class RenderPosts(Task):
"show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
"demote_headers": self.site.config['DEMOTE_HEADERS'],
}
+ self.tl_changed = False
yield self.group_task()
+ def tl_ch():
+ self.tl_changed = True
+
+ yield {
+ 'basename': self.name,
+ 'name': 'timeline_changes',
+ 'actions': [tl_ch],
+ 'uptodate': [utils.config_changed({1: kw['timeline']})],
+ }
+
for lang in kw["translations"]:
deps_dict = copy(kw)
deps_dict.pop('timeline')
for post in kw['timeline']:
+
dest = post.translated_base_path(lang)
+ file_dep = [p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")]
task = {
'basename': self.name,
'name': dest,
- 'file_dep': post.fragment_deps(lang),
+ 'file_dep': file_dep,
'targets': [dest],
'actions': [(post.compile, (lang, )),
- (rest_deps, (post,)),
+ (update_deps, (post, lang, )),
],
'clean': True,
- 'uptodate': [utils.config_changed(deps_dict)],
+ 'uptodate': [
+ utils.config_changed(deps_dict, 'nikola.plugins.task.posts'),
+ lambda p=post, l=lang: self.dependence_on_timeline(p, l)
+ ] + post.fragment_deps_uptodate(lang),
+ 'task_dep': ['render_posts:timeline_changes']
}
- yield task
+
+ # Apply filters specified in the metadata
+ ff = [x.strip() for x in post.meta('filters', lang).split(',')]
+ flist = []
+ 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
+ else:
+ flist.append(f)
+ yield utils.apply_filters(task, {os.path.splitext(dest): flist})
+
+ def dependence_on_timeline(self, post, lang):
+ if "####MAGIC####TIMELINE" not in post.fragment_deps(lang):
+ return True # No dependency on timeline
+ elif self.tl_changed:
+ return False # Timeline changed
+ return True
diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin
index 826f3d8..0228c70 100644
--- a/nikola/plugins/task/redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -4,7 +4,7 @@ Module = redirect
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Create redirect pages.
diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py
index e1134bf..428dd5a 100644
--- a/nikola/plugins/task/redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,7 +24,8 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import io
+from __future__ import unicode_literals
+
import os
from nikola.plugin_categories import Task
@@ -42,26 +43,18 @@ class Redirect(Task):
kw = {
'redirections': self.site.config['REDIRECTIONS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
}
yield self.group_task()
if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src)
- yield {
+ yield utils.apply_filters({
'basename': self.name,
'name': src_path,
'targets': [src_path],
- 'actions': [(create_redirect, (src_path, dst))],
+ 'actions': [(utils.create_redirect, (src_path, dst))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
-
-def create_redirect(src, dst):
- utils.makedirs(os.path.dirname(src))
- with io.open(src, "w+", encoding="utf8") as fd:
- fd.write('<!DOCTYPE html><head><title>Redirecting...</title>'
- '<meta name="robots" content="noindex">'
- '<meta http-equiv="refresh" content="0; '
- 'url={0}"></head><body><p>Page moved <a href="{0}">here</a></p></body>'.format(dst))
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.redirect')],
+ }, kw["filters"])
diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin
index 60b50fb..b4b43a3 100644
--- a/nikola/plugins/task/robots.plugin
+++ b/nikola/plugins/task/robots.plugin
@@ -4,7 +4,7 @@ Module = robots
[Documentation]
Author = Daniel Aleksandersen
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate /robots.txt exclusion file and promote sitemap.
diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py
index b229d37..2f25a21 100644
--- a/nikola/plugins/task/robots.py
+++ b/nikola/plugins/task/robots.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -48,7 +48,8 @@ class RobotsFile(LateTask):
"site_url": self.site.config["SITE_URL"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"files_folders": self.site.config['FILES_FOLDERS'],
- "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"]
+ "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
+ "filters": self.site.config["FILTERS"],
}
sitemapindex_url = urljoin(kw["base_url"], "sitemapindex.xml")
@@ -68,15 +69,15 @@ class RobotsFile(LateTask):
yield self.group_task()
if not utils.get_asset_path("robots.txt", [], files_folders=kw["files_folders"]):
- yield {
+ yield utils.apply_filters({
"basename": self.name,
"name": robots_path,
"targets": [robots_path],
"actions": [(write_robots)],
- "uptodate": [utils.config_changed(kw)],
+ "uptodate": [utils.config_changed(kw, 'nikola.plugins.task.robots')],
"clean": True,
"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 fie.')
else:
diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin
index 7206a43..56f0bf4 100644
--- a/nikola/plugins/task/rss.plugin
+++ b/nikola/plugins/task/rss.plugin
@@ -4,7 +4,7 @@ Module = rss
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate RSS feeds.
diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py
index b16ed48..26a4da1 100644
--- a/nikola/plugins/task/rss.py
+++ b/nikola/plugins/task/rss.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -51,6 +51,7 @@ class GenerateRSS(Task):
"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"],
"rss_teasers": self.site.config["RSS_TEASERS"],
@@ -59,6 +60,7 @@ class GenerateRSS(Task):
"feed_length": self.site.config['FEED_LENGTH'],
"tzinfo": self.site.tzinfo,
"rss_read_more_link": self.site.config["RSS_READ_MORE_LINK"],
+ "rss_links_append_query": self.site.config["RSS_LINKS_APPEND_QUERY"],
}
self.site.scan_posts()
# Check for any changes in the state of use_in_feeds for any post.
@@ -71,16 +73,18 @@ class GenerateRSS(Task):
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[:10]
+ posts = self.site.posts[:kw['feed_length']]
else:
- posts = [x for x in self.site.posts if x.is_translation_available(lang)][:10]
+ 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('/'))
- yield {
+ task = {
'basename': 'generate_rss',
'name': os.path.normpath(output_name),
'file_dep': deps,
@@ -88,12 +92,14 @@ class GenerateRSS(Task):
'actions': [(utils.generic_rss_renderer,
(lang, kw["blog_title"](lang), kw["site_url"],
kw["blog_description"](lang), posts, output_name,
- kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], feed_url))],
+ kw["rss_teasers"], kw["rss_plain"], kw['feed_length'], feed_url,
+ None, kw["rss_links_append_query"]))],
'task_dep': ['render_posts'],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.rss')] + deps_uptodate,
}
+ yield utils.apply_filters(task, kw['filters'])
def rss_path(self, name, lang):
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
diff --git a/nikola/plugins/task/scale_images.plugin b/nikola/plugins/task/scale_images.plugin
new file mode 100644
index 0000000..c0f0f28
--- /dev/null
+++ b/nikola/plugins/task/scale_images.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = scale_images
+Module = scale_images
+
+[Documentation]
+Author = Pelle Nilsson
+Version = 1.0
+Website = http://getnikola.com
+Description = Create down-scaled images and thumbnails.
diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py
new file mode 100644
index 0000000..f97027e
--- /dev/null
+++ b/nikola/plugins/task/scale_images.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2014-2015 Pelle Nilsson and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import os
+
+from nikola.plugin_categories import Task
+from nikola.image_processing import ImageProcessor
+from nikola import utils
+
+
+class ScaleImage(Task, ImageProcessor):
+ """Copy static files into the output folder."""
+
+ name = "scale_images"
+
+ def set_site(self, site):
+ self.logger = utils.get_logger('scale_images', site.loghandlers)
+ return super(ScaleImage, self).set_site(site)
+
+ def process_tree(self, src, dst):
+ """Processes all images in a src tree and put the (possibly) rescaled
+ images in the dst folder."""
+ ignore = set(['.svn'])
+ 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))
+ yield {
+ 'name': dst_file,
+ 'file_dep': [src_file],
+ 'targets': [dst_file, thumb_file],
+ 'actions': [(self.process_image, (src_file, dst_file, thumb_file))],
+ 'clean': True,
+ }
+
+ def process_image(self, src, dst, thumb):
+ self.resize_image(src, dst, self.kw['max_image_size'], False)
+ self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False)
+
+ def gen_tasks(self):
+ """Copy static files into the output folder."""
+
+ self.kw = {
+ 'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'image_folders': self.site.config['IMAGE_FOLDERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
+ }
+
+ self.image_ext_list = self.image_ext_list_builtin
+ self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
+
+ yield self.group_task()
+ for src in self.kw['image_folders']:
+ dst = self.kw['output_folder']
+ filters = self.kw['filters']
+ real_dst = os.path.join(dst, self.kw['image_folders'][src])
+ for task in self.process_tree(src, real_dst):
+ task['basename'] = self.name
+ task['uptodate'] = [utils.config_changed(self.kw)]
+ yield utils.apply_filters(task, filters)
diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index 2cd8195..0b992b8 100644
--- a/nikola/plugins/task/sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -4,7 +4,7 @@ Module = sitemap
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Generate google sitemap.
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py
index 943e9b2..92d557d 100644
--- a/nikola/plugins/task/sitemap/__init__.py
+++ b/nikola/plugins/task/sitemap/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -36,7 +36,7 @@ except ImportError:
import urllib.robotparser as robotparser # NOQA
from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed
+from nikola.utils import config_changed, apply_filters
urlset_header = """<?xml version="1.0" encoding="UTF-8"?>
@@ -49,7 +49,7 @@ urlset_header = """<?xml version="1.0" encoding="UTF-8"?>
loc_format = """ <url>
<loc>{0}</loc>
- <lastmod>{1}</lastmod>
+ <lastmod>{1}</lastmod>{2}
</url>
"""
@@ -69,6 +69,9 @@ sitemap_format = """ <sitemap>
</sitemap>
"""
+alternates_format = """\n <xhtml:link rel="alternate" hreflang="{0}" href="{1}" />"""
+
+
sitemapindex_footer = "</sitemapindex>"
@@ -111,8 +114,10 @@ class Sitemap(LateTask):
"strip_indexes": self.site.config["STRIP_INDEXES"],
"index_file": self.site.config["INDEX_FILE"],
"sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"],
- "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm', '.xml', '.rss']),
- "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"]
+ "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.xml', '.rss']),
+ "robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
+ "filters": self.site.config["FILTERS"],
+ "translations": self.site.config["TRANSLATIONS"],
}
output = kw['output_folder']
@@ -136,7 +141,17 @@ class Sitemap(LateTask):
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
- urlset[loc] = loc_format.format(loc, lastmod)
+ post = self.site.post_per_file.get(path + kw['index_file'])
+ if post and (post.is_draft or post.is_private or post.publish_later):
+ continue
+ alternates = []
+ if post:
+ for lang in kw['translations']:
+ alt_url = post.permalink(lang=lang, absolute=True)
+ if loc == alt_url:
+ continue
+ alternates.append(alternates_format.format(lang, alt_url))
+ urlset[loc] = loc_format.format(loc, lastmod, ''.join(alternates))
for fname in files:
if kw['strip_indexes'] and fname == kw['index_file']:
continue # We already mapped the folder
@@ -148,20 +163,30 @@ class Sitemap(LateTask):
continue
if not robot_fetch(path):
continue
+
+ # read in binary mode to make ancient files work
+ fh = open(real_path, 'rb')
+ filehead = fh.read(1024)
+ fh.close()
+
if path.endswith('.html') or path.endswith('.htm'):
- try:
- if u'<!doctype html' not in io.open(real_path, 'r', encoding='utf8').read(1024).lower():
- # ignores "html" files without doctype
- # alexa-verify, google-site-verification, etc.
- continue
- except UnicodeDecodeError:
- # ignore ancient files
- # most non-utf8 files are worthless anyways
+ """ ignores "html" files without doctype """
+ if b'<!doctype html' not in filehead.lower():
continue
- """ put RSS in sitemapindex[] instead of in urlset[], sitemap_path is included after it is generated """
- if path.endswith('.xml') or path.endswith('.rss'):
- filehead = io.open(real_path, 'r', encoding='utf8').read(512)
- if u'<rss' in filehead or (u'<urlset' in filehead and path != sitemap_path):
+
+ """ ignores "html" files with noindex robot directives """
+ robots_directives = [b'<meta content="noindex" name="robots"',
+ b'<meta content="none" name="robots"',
+ b'<meta name="robots" content="noindex"',
+ b'<meta name="robots" content="none"']
+ if any([robot_directive in filehead.lower() for robot_directive in robots_directives]):
+ continue
+
+ # put Atom and RSS in sitemapindex[] instead of in urlset[],
+ # sitemap_path is included after it is generated
+ if path.endswith('.xml') or path.endswith('.atom') or path.endswith('.rss'):
+ known_elm_roots = (b'<feed', b'<rss', b'<urlset')
+ if any([elm_root in filehead.lower() for elm_root in known_elm_roots]) and path != sitemap_path:
path = path.replace(os.sep, '/')
lastmod = self.get_lastmod(real_path)
loc = urljoin(base_url, base_path + path)
@@ -175,7 +200,14 @@ class Sitemap(LateTask):
path = path.replace(os.sep, '/')
lastmod = self.get_lastmod(real_path)
loc = urljoin(base_url, base_path + path)
- urlset[loc] = loc_format.format(loc, lastmod)
+ alternates = []
+ if post:
+ for lang in kw['translations']:
+ alt_url = post.permalink(lang=lang, absolute=True)
+ if loc == alt_url:
+ continue
+ alternates.append(alternates_format.format(lang, alt_url))
+ urlset[loc] = loc_format.format(loc, lastmod, '\n'.join(alternates))
def robot_fetch(path):
for rule in kw["robots_exclusions"]:
@@ -208,7 +240,27 @@ class Sitemap(LateTask):
# to scan locations.
def scan_locs_task():
scan_locs()
- return {'locations': list(urlset.keys()) + list(sitemapindex.keys())}
+
+ # Generate a list of file dependencies for the actual generation
+ # task, so rebuilds are triggered. (Issue #1032)
+ output = kw["output_folder"]
+ file_dep = []
+
+ for i in urlset.keys():
+ p = os.path.join(output, urlparse(i).path.replace(base_path, '', 1))
+ if not p.endswith('sitemap.xml') and not os.path.isdir(p):
+ file_dep.append(p)
+ if os.path.isdir(p) and os.path.exists(os.path.join(p, 'index.html')):
+ file_dep.append(p + 'index.html')
+
+ for i in sitemapindex.keys():
+ p = os.path.join(output, urlparse(i).path.replace(base_path, '', 1))
+ if not p.endswith('sitemap.xml') and not os.path.isdir(p):
+ file_dep.append(p)
+ if os.path.isdir(p) and os.path.exists(os.path.join(p, 'index.html')):
+ file_dep.append(p + 'index.html')
+
+ return {'file_dep': file_dep}
yield {
"basename": "_scan_locs",
@@ -217,29 +269,29 @@ class Sitemap(LateTask):
}
yield self.group_task()
- yield {
+ yield apply_filters({
"basename": "sitemap",
"name": sitemap_path,
"targets": [sitemap_path],
"actions": [(write_sitemap,)],
- "uptodate": [config_changed(kw)],
+ "uptodate": [config_changed(kw, 'nikola.plugins.task.sitemap:write')],
"clean": True,
"task_dep": ["render_site"],
"calc_dep": ["_scan_locs:sitemap"],
- }
- yield {
+ }, kw['filters'])
+ yield apply_filters({
"basename": "sitemap",
"name": sitemapindex_path,
"targets": [sitemapindex_path],
"actions": [(write_sitemapindex,)],
- "uptodate": [config_changed(kw)],
+ "uptodate": [config_changed(kw, 'nikola.plugins.task.sitemap:write_index')],
"clean": True,
"file_dep": [sitemap_path]
- }
+ }, kw['filters'])
def get_lastmod(self, p):
if self.site.invariant:
- return '2014-01-01'
+ return '2038-01-01'
else:
return datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin
index 6224e48..5560df6 100644
--- a/nikola/plugins/task/sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -4,7 +4,7 @@ Module = sources
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Copy page sources into the output.
diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py
index 4c669c2..840a31c 100644
--- a/nikola/plugins/task/sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -49,6 +49,7 @@ class Sources(Task):
"translations": self.site.config["TRANSLATIONS"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"default_lang": self.site.config["DEFAULT_LANG"],
+ "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
@@ -56,6 +57,8 @@ class Sources(Task):
if self.site.config['COPY_SOURCES']:
for lang in kw["translations"]:
for post in self.site.timeline:
+ if not kw["show_untranslated_posts"] and lang not in post.translated_to:
+ continue
if post.meta('password'):
continue
output_name = os.path.join(
@@ -77,5 +80,5 @@ class Sources(Task):
'targets': [output_name],
'actions': [(utils.copy_file, (source, output_name))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.sources')],
}
diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin
index f01e0f8..4ac3800 100644
--- a/nikola/plugins/task/tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -4,7 +4,7 @@ Module = tags
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Render the tag pages and feeds.
diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py
index 8d43f13..832ceff 100644
--- a/nikola/plugins/task/tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,6 +27,8 @@
from __future__ import unicode_literals
import json
import os
+import sys
+import natsort
try:
from urlparse import urljoin
except ImportError:
@@ -43,9 +45,12 @@ class RenderTags(Task):
def set_site(self, 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)
@@ -56,18 +61,26 @@ class RenderTags(Task):
"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'],
- "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'],
- "index_teasers": self.site.config['INDEX_TEASERS'],
+ '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'],
"rss_teasers": self.site.config["RSS_TEASERS"],
"rss_plain": self.site.config["RSS_PLAIN"],
+ "rss_link_append_query": self.site.config["RSS_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'],
}
self.site.scan_posts()
@@ -78,6 +91,32 @@ class RenderTags(Task):
if not self.site.posts_per_tag and not self.site.posts_per_category:
return
+ if kw['category_path'] == kw['tag_path']:
+ tags = {self.slugify_tag_name(tag): tag for tag in self.site.posts_per_tag.keys()}
+ cats = {tuple(self.slugify_category_name(category)): 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}'!".format('/'.join(categories[slug]), tags[slug], slug))
+ sys.exit(1)
+
+ # Test for category slug clashes
+ categories = {}
+ for category in self.site.posts_per_category.keys():
+ slug = tuple(self.slugify_category_name(category))
+ for part in slug:
+ if len(part) == 0:
+ utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
+ sys.exit(1)
+ if slug in categories:
+ other_category = categories[slug]
+ utils.LOGGER.error('You have categories that are too similar: {0} and {1}'.format(category, other_category))
+ 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]])))
+ sys.exit(1)
+ categories[slug] = category
+
tag_list = list(self.site.posts_per_tag.items())
cat_list = list(self.site.posts_per_category.items())
@@ -92,7 +131,7 @@ class RenderTags(Task):
if kw["generate_rss"]:
yield self.tag_rss(tag, lang, filtered_posts, kw, is_category)
# Render HTML
- if kw['tag_pages_are_indexes']:
+ 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)
@@ -101,19 +140,19 @@ class RenderTags(Task):
for task in render_lists(tag, posts, False):
yield task
- for tag, posts in cat_list:
- if tag == '': # This is uncategorized posts
- continue
- for task in render_lists(tag, posts, True):
+ 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.base_path.replace('cache', '')}
+ '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(
@@ -126,48 +165,59 @@ class RenderTags(Task):
with open(output_name, 'w+') as fd:
json.dump(data, fd)
- task = {
- 'basename': str(self.name),
- 'name': str(output_name)
- }
+ if self.site.config['WRITE_TAG_CLOUD']:
+ task = {
+ 'basename': str(self.name),
+ 'name': str(output_name)
+ }
- task['uptodate'] = [utils.config_changed(tag_cloud_data)]
- task['targets'] = [output_name]
- task['actions'] = [(write_tag_data, [tag_cloud_data])]
- task['clean'] = True
- yield task
+ 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'])
- def list_tags_page(self, kw):
+ def _create_tags_page(self, kw, include_tags=True, include_categories=True):
"""a global "all your tags/categories" page for each language"""
- tags = list(self.site.posts_per_tag.keys())
- categories = list(self.site.posts_per_category.keys())
- # We want our tags to be sorted case insensitive
- tags.sort(key=lambda a: a.lower())
- categories.sort(key=lambda a: a.lower())
- if categories != ['']:
- has_categories = True
- else:
- has_categories = False
+ tags = natsort.natsorted([tag for tag in self.site.posts_per_tag.keys()
+ if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]],
+ alg=natsort.ns.F | natsort.ns.IC)
+ categories = [cat.category_name for cat in self.site.category_hierarchy]
+ has_tags = (tags != []) and include_tags
+ has_categories = (categories != []) and include_categories
template_name = "tags.tmpl"
- kw['tags'] = tags
- kw['categories'] = categories
+ kw = kw.copy()
+ if include_tags:
+ kw['tags'] = tags
+ if include_categories:
+ kw['categories'] = categories
for lang in kw["translations"]:
output_name = os.path.join(
- kw['output_folder'], self.site.path('tag_index', None, lang))
+ kw['output_folder'], self.site.path('tag_index' if has_tags else 'category_index', None, lang))
output_name = output_name
context = {}
- if has_categories:
+ 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"]
- context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
- in 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", None, lang)
+ context["permalink"] = self.site.link("tag_index" if has_tags else "category_index", None, lang)
context["description"] = context["title"]
task = self.site.generic_post_list_renderer(
lang,
@@ -177,73 +227,66 @@ class RenderTags(Task):
kw['filters'],
context,
)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [utils.config_changed(task_cfg)]
+ 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):
+ """a global "all your tags/categories" page for each language"""
+ if self.site.config['TAG_PATH'] == self.site.config['CATEGORY_PATH']:
+ yield self._create_tags_page(kw, True, True)
+ else:
+ yield self._create_tags_page(kw, False, True)
+ yield self._create_tags_page(kw, 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_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]
+
def tag_page_as_index(self, tag, lang, post_list, kw, is_category):
"""render a sort of index page collection using only this
tag's posts."""
kind = "category" if is_category else "tag"
- def page_name(tagname, i, lang):
- """Given tag, n, returns a page name."""
- name = self.site.path(kind, tag, lang)
- if i:
- name = name.replace('.html', '-{0}.html'.format(i))
- return name
-
- # FIXME: deduplicate this with render_indexes
+ 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)
+
+ 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)
+
+ 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" """
+ """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 = kw["messages"][lang]["Posts about %s"] % title
+ context_source["description"] = self._get_description(tag, is_category, lang)
+ if is_category:
+ context_source["subcategories"] = self._get_subcategories(tag)
template_name = "tagindex.tmpl"
- # Split in smaller lists
- lists = []
- while post_list:
- lists.append(post_list[:kw["index_display_post_count"]])
- post_list = post_list[kw["index_display_post_count"]:]
- num_pages = len(lists)
- for i, post_list in enumerate(lists):
- context = {}
- if kw["generate_rss"]:
- # On a tag page, the feeds include the tag's feeds
- rss_link = ("""<link rel="alternate" type="application/rss+xml" """
- """type="application/rss+xml" title="RSS for tag """
- """{0} ({1})" href="{2}">""".format(
- tag, lang, self.site.link(kind + "_rss", tag, lang)))
- context['rss_link'] = rss_link
- output_name = os.path.join(kw['output_folder'],
- page_name(tag, i, lang))
- context["title"] = kw["messages"][lang][
- "Posts about %s"] % tag
- context["prevlink"] = None
- context["nextlink"] = None
- context['index_teasers'] = kw['index_teasers']
- if i > 1:
- context["prevlink"] = os.path.basename(
- page_name(tag, i - 1, lang))
- if i == 1:
- context["prevlink"] = os.path.basename(
- page_name(tag, 0, lang))
- if i < num_pages - 1:
- context["nextlink"] = os.path.basename(
- page_name(tag, i + 1, lang))
- context["permalink"] = self.site.link(kind, tag, lang)
- context["tag"] = tag
- context["description"] = context["title"]
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [utils.config_changed(task_cfg)]
- task['basename'] = str(self.name)
- yield task
+ yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path)
def tag_page_as_list(self, tag, lang, post_list, kw, is_category):
"""We render a single flat link list with this tag's posts"""
@@ -253,12 +296,18 @@ class RenderTags(Task):
kind, tag, lang))
context = {}
context["lang"] = lang
- context["title"] = kw["messages"][lang]["Posts about %s"] % tag
+ 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"] = kw["messages"][lang]["Posts about %s"] % title
context["posts"] = post_list
context["permalink"] = self.site.link(kind, tag, lang)
- context["tag"] = tag
context["kind"] = kind
- context["description"] = context["title"]
+ context["description"] = self._get_description(tag, is_category, lang)
+ if is_category:
+ context["subcategories"] = self._get_subcategories(tag)
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -267,8 +316,7 @@ class RenderTags(Task):
kw['filters'],
context,
)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [utils.config_changed(task_cfg)]
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')]
task['basename'] = str(self.name)
yield task
@@ -281,26 +329,29 @@ class RenderTags(Task):
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)
- return {
+ 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), tag),
+ (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, is_category)),
kw["site_url"], None, post_list,
output_name, kw["rss_teasers"], kw["rss_plain"], kw['feed_length'],
- feed_url))],
+ feed_url, None, kw["rss_link_append_query"]))],
'clean': True,
- 'uptodate': [utils.config_changed(kw)],
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate,
'task_dep': ['render_posts'],
}
+ return utils.apply_filters(task, kw['filters'])
- def slugify_name(self, name):
+ def slugify_tag_name(self, name):
if self.site.config['SLUG_TAG_PATH']:
name = utils.slugify(name)
return name
@@ -310,30 +361,64 @@ class RenderTags(Task):
self.site.config['TAG_PATH'],
self.site.config['INDEX_FILE']] if _f]
+ def category_index_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH'],
+ self.site.config['INDEX_FILE']] if _f]
+
def tag_path(self, name, lang):
if self.site.config['PRETTY_URLS']:
return [_f for _f in [
self.site.config['TRANSLATIONS'][lang],
self.site.config['TAG_PATH'],
- self.slugify_name(name),
+ self.slugify_tag_name(name),
self.site.config['INDEX_FILE']] if _f]
else:
return [_f for _f in [
self.site.config['TRANSLATIONS'][lang],
self.site.config['TAG_PATH'],
- self.slugify_name(name) + ".html"] if _f]
+ self.slugify_tag_name(name) + ".html"] if _f]
+
+ def tag_atom_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".atom"] if
+ _f]
def tag_rss_path(self, name, lang):
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'], self.slugify_name(name) + ".xml"] if
+ self.site.config['TAG_PATH'], self.slugify_tag_name(name) + ".xml"] if
_f]
+ def slugify_category_name(self, name):
+ 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) 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):
+ if self.site.config['PRETTY_URLS']:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self.slugify_category_name(name) + [self.site.config['INDEX_FILE']]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".html")
+
+ def category_atom_path(self, name, lang):
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".html"] if
- _f]
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".atom")
def category_rss_path(self, name, lang):
return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".xml"] if
- _f]
+ self.site.config['CATEGORY_PATH']] if
+ _f] + self._add_extension(self.slugify_category_name(name), ".xml")
diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py
index 6ad8bac..a1d17a6 100644
--- a/nikola/plugins/template/__init__.py
+++ b/nikola/plugins/template/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 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 53b0fec..0bdcb94 100644
--- a/nikola/plugins/template/jinja.plugin
+++ b/nikola/plugins/template/jinja.plugin
@@ -4,6 +4,6 @@ Module = jinja
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Support for Jinja2 templates.
diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py
index 5156f38..82e8397 100644
--- a/nikola/plugins/template/jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -114,8 +114,7 @@ class JinjaTemplates(TemplateSystem):
ast = self.lookup.parse(source)
dep_names = meta.find_referenced_templates(ast)
for dep_name in dep_names:
- if (dep_name not in visited_templates
- and dep_name is not None):
+ if (dep_name not in visited_templates and dep_name is not None):
visited_templates.add(dep_name)
queue.append(dep_name)
self.dependency_cache[template_name] = deps
diff --git a/nikola/plugins/template/mako.plugin b/nikola/plugins/template/mako.plugin
index 71f2c71..2fe6d98 100644
--- a/nikola/plugins/template/mako.plugin
+++ b/nikola/plugins/template/mako.plugin
@@ -4,6 +4,6 @@ Module = mako
[Documentation]
Author = Roberto Alsina
-Version = 0.1
+Version = 1.0
Website = http://getnikola.com
Description = Support for Mako templates.
diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py
index b9d856e..e5545f6 100644
--- a/nikola/plugins/template/mako.py
+++ b/nikola/plugins/template/mako.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -115,7 +115,7 @@ class MakoTemplates(TemplateSystem):
def render_template_to_string(self, template, context):
""" Render template to a string using context. """
- context = context.update(self.filters)
+ context.update(self.filters)
return Template(template).render(**context)
diff --git a/nikola/post.py b/nikola/post.py
index 350014a..466d5e0 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,6 +29,8 @@ 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
@@ -37,6 +39,8 @@ try:
except ImportError:
from urllib.parse import urljoin # NOQA
+from . import utils
+
import dateutil.tz
import lxml.html
import natsort
@@ -67,6 +71,7 @@ from .rc4 import rc4
__all__ = ['Post']
TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE)
+_UPGRADE_METADATA_ADVERTISED = False
class Post(object):
@@ -122,13 +127,18 @@ class Post(object):
self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS']
self._template_name = template_name
self.is_two_file = True
+ self.newstylemeta = True
self.hyphenate = self.config['HYPHENATE']
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)
- default_metadata = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'])
+ 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
@@ -140,7 +150,9 @@ class Post(object):
if lang != self.default_lang:
meta = defaultdict(lambda: '')
meta.update(default_metadata)
- meta.update(get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'], lang))
+ _meta, _nsm = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'], lang)
+ self.newstylemeta = self.newstylemeta and _nsm
+ meta.update(_meta)
self.meta[lang] = meta
if not self.is_translation_available(self.default_lang):
@@ -157,6 +169,14 @@ class Post(object):
default_metadata['date'] = datetime.datetime.utcfromtimestamp(
os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(tzinfo)
+ # If time zone is set, build localized datetime.
+ self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo)
+
+ if 'updated' not in default_metadata:
+ default_metadata['updated'] = default_metadata.get('date', None)
+
+ self.updated = to_datetime(default_metadata['updated'])
+
if 'title' not in default_metadata or 'slug' not in default_metadata \
or 'date' not in default_metadata:
raise OSError("You must set a title (found '{0}'), a slug (found "
@@ -170,9 +190,6 @@ class Post(object):
# default value is 'text'
default_metadata['type'] = 'text'
- # If time zone is set, build localized datetime.
- self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo)
-
self.publish_later = False if self.current_time is None else self.date >= self.current_time
is_draft = False
@@ -180,9 +197,10 @@ class Post(object):
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(',')])))
+ 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 self._tags[lang]:
+ 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')
@@ -206,11 +224,26 @@ class Post(object):
self.use_in_feeds = use_in_feeds and not is_draft and not is_private \
and not self.publish_later
- # If mathjax is a tag, then enable mathjax rendering support
- self.is_mathjax = 'mathjax' in self.tags
+ # If mathjax is a tag, or it's a ipynb post, then enable mathjax rendering support
+ self.is_mathjax = ('mathjax' in self.tags) or (self.compiler.name == 'ipynb')
+
+ # Register potential extra dependencies
+ self.compiler.register_extra_dependencies(self)
def __repr__(self):
- return '<Post: {0}>'.format(self.source_path)
+ # 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'))
+ clean_meta = {}
+ for k, v in self.meta.items():
+ sub_meta = {}
+ clean_meta[k] = sub_meta
+ 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'))
+ return '<Post: {0!r} {1}>'.format(self.source_path, m.hexdigest())
def _has_pretty_url(self, lang):
if self.pretty_urls and \
@@ -274,14 +307,20 @@ class Post(object):
def template_name(self):
return self.meta('template') or self._template_name
- def formatted_date(self, date_format):
+ def formatted_date(self, date_format, date=None):
"""Return the formatted date, as unicode."""
- fmt_date = self.date.strftime(date_format)
+ if date:
+ fmt_date = date.strftime(date_format)
+ else:
+ fmt_date = self.date.strftime(date_format)
# Issue #383, this changes from py2 to py3
if isinstance(fmt_date, bytes_str):
fmt_date = fmt_date.decode('utf8')
return fmt_date
+ def formatted_updated(self, date_format):
+ return self.formatted_date(date_format, self.updated)
+
def title(self, lang=None):
"""Return localized title.
@@ -313,8 +352,76 @@ class Post(object):
lang = nikola.utils.LocaleBorg().current_lang
return self.meta[lang]['description']
+ def add_dependency(self, dependency, add='both', lang=None):
+ """Adds a file dependency for tasks using that post.
+
+ The ``dependency`` should be a string specifying a path, or a callable
+ which returns such a string or a list of strings.
+
+ The ``add`` parameter can be 'both', 'fragment' or 'page', to indicate
+ that this dependency shall be used
+ * when rendering the fragment to HTML ('fragment' and 'both'), or
+ * when creating a page with parts of the ``Post`` embedded, which
+ includes the HTML resulting from compiling the fragment ('page' or
+ 'both').
+
+ If ``lang`` is not specified, this dependency is added for all languages."""
+ if add not in {'fragment', 'page', 'both'}:
+ raise Exception("Add parameter is '{0}', but must be either 'fragment', 'page', or 'both'.".format(add))
+ if add == 'fragment' or add == 'both':
+ self._dependency_file_fragment[lang].append((type(dependency) != str, dependency))
+ if add == 'page' or add == 'both':
+ self._dependency_file_page[lang].append((type(dependency) != str, dependency))
+
+ def add_dependency_uptodate(self, dependency, is_callable=False, add='both', lang=None):
+ """Adds a dependency for task's ``uptodate`` for tasks using that post.
+
+ This can be for example an ``utils.config_changed`` object, or a list of
+ such objects.
+
+ The ``is_callable`` parameter specifies whether ``dependency`` is a
+ callable which generates an entry or a list of entries for the ``uptodate``
+ list, or whether it is an entry which can directly be added (as a single
+ object or a list of objects).
+
+ The ``add`` parameter can be 'both', 'fragment' or 'page', to indicate
+ that this dependency shall be used
+ * when rendering the fragment to HTML ('fragment' and 'both'), or
+ * when creating a page with parts of the ``Post`` embedded, which
+ includes the HTML resulting from compiling the fragment ('page' or
+ 'both').
+
+ If ``lang`` is not specified, this dependency is added for all languages.
+
+ Example:
+
+ post.add_dependency_uptodate(
+ utils.config_changed({1: some_data}, 'uniqueid'), False, 'page')
+
+ """
+ if add == 'fragment' or add == 'both':
+ self._dependency_uptodate_fragment[lang].append((is_callable, dependency))
+ if add == 'page' or add == 'both':
+ self._dependency_uptodate_page[lang].append((is_callable, dependency))
+
+ def _get_dependencies(self, deps_list):
+ deps = []
+ for dep in deps_list:
+ if dep[0]:
+ # callable
+ result = dep[1]()
+ else:
+ # can add directly
+ result = dep[1]
+ # if result is a list, add its contents
+ if type(result) == list:
+ deps.extend(result)
+ else:
+ deps.append(result)
+ return deps
+
def deps(self, lang):
- """Return a list of dependencies to build this post's page."""
+ """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)
@@ -324,6 +431,19 @@ class Post(object):
cand_2 = get_translation_candidate(self.config, self.base_path, lang)
if os.path.exists(cand_1):
deps.extend([cand_1, cand_2])
+ deps += self._get_dependencies(self._dependency_file_page[lang])
+ deps += self._get_dependencies(self._dependency_file_page[None])
+ return deps
+
+ def deps_uptodate(self, lang):
+ """Return a list of uptodate dependencies to build this post's page.
+
+ These dependencies should be included in ``uptodate`` for the task
+ which generates the page."""
+ deps = []
+ deps += self._get_dependencies(self._dependency_uptodate_page[lang])
+ deps += self._get_dependencies(self._dependency_uptodate_page[None])
+ deps.append(utils.config_changed({1: sorted(self.compiler.config_dependencies)}, 'nikola.post.Post.deps_uptodate:compiler:' + self.source_path))
return deps
def compile(self, lang):
@@ -347,34 +467,41 @@ class Post(object):
dest,
self.is_two_file),
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(
self.source_path, self.date))
- def extra_deps(self):
- """get extra depepencies from .dep files
- This file is created by ReST
- """
- dep_path = self.base_path + '.dep'
- if os.path.isfile(dep_path):
- with io.open(dep_path, 'r+', encoding='utf8') as depf:
- return [l.strip() for l in depf.readlines()]
- return []
-
def fragment_deps(self, lang):
- """Return a list of dependencies to build this post's fragment."""
+ """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)
if os.path.isfile(self.metadata_path):
deps.append(self.metadata_path)
- deps.extend(self.extra_deps())
lang_deps = []
if lang != self.default_lang:
lang_deps = [get_translation_candidate(self.config, d, lang) for d in deps]
deps += lang_deps
- return [d for d in deps if os.path.exists(d)]
+ deps = [d for d in deps if os.path.exists(d)]
+ deps += self._get_dependencies(self._dependency_file_fragment[lang])
+ deps += self._get_dependencies(self._dependency_file_fragment[None])
+ return deps
+
+ def fragment_deps_uptodate(self, lang):
+ """Return a list of file dependencies to build this post's fragment."""
+ deps = []
+ deps += self._get_dependencies(self._dependency_uptodate_fragment[lang])
+ deps += self._get_dependencies(self._dependency_uptodate_fragment[None])
+ deps.append(utils.config_changed({1: sorted(self.compiler.config_dependencies)}, 'nikola.post.Post.deps_uptodate:compiler:' + self.source_path))
+ return deps
def is_translation_available(self, lang):
"""Return true if the translation actually exists."""
@@ -408,7 +535,8 @@ class Post(object):
else:
return get_translation_candidate(self.config, self.base_path, sorted(self.translated_to)[0])
- def text(self, lang=None, teaser_only=False, strip_html=False, show_read_more_link=True, rss_read_more_link=False):
+ def text(self, lang=None, teaser_only=False, strip_html=False, show_read_more_link=True,
+ rss_read_more_link=False, rss_links_append_query=None):
"""Read the post file for that language and return its contents.
teaser_only=True breaks at the teaser marker and returns only the teaser.
@@ -424,8 +552,16 @@ class Post(object):
if lang is None:
lang = nikola.utils.LocaleBorg().current_lang
file_name = 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
+ # and the post should not just refuse to give it.
+ if not os.path.isfile(file_name):
+ self.compile(lang)
+
with io.open(file_name, "r", encoding="utf8") as post_file:
data = post_file.read().strip()
+
if self.compiler.extension() == '.php':
return data
try:
@@ -458,7 +594,7 @@ class Post(object):
else:
l = self.config['RSS_READ_MORE_LINK'](lang) if rss_read_more_link else self.config['INDEX_READ_MORE_LINK'](lang)
teaser += l.format(
- link=self.permalink(lang),
+ link=self.permalink(lang, query=rss_links_append_query),
read_more=self.messages[lang]["Read more"],
min_remaining_read=self.messages[lang]["%d min remaining to read"] % (self.remaining_reading_time),
reading_time=self.reading_time,
@@ -498,7 +634,12 @@ class Post(object):
text = self.text(strip_html=True)
words_per_minute = 220
words = len(text.split())
- self._reading_time = int(ceil(words / words_per_minute)) or 1
+ markup = lxml.html.fromstring(self.text(strip_html=False))
+ embeddables = [".//img", ".//picture", ".//video", ".//audio", ".//object", ".//iframe"]
+ media_time = 0
+ for embedded in embeddables:
+ media_time += (len(markup.findall(embedded)) * 0.33) # +20 seconds
+ self._reading_time = int(ceil((words / words_per_minute) + media_time)) or 1
return self._reading_time
@property
@@ -574,9 +715,11 @@ class Post(object):
self.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 permalink(self, lang=None, absolute=False, extension='.html'):
+ def permalink(self, lang=None, absolute=False, extension='.html', query=None):
if lang is None:
lang = nikola.utils.LocaleBorg().current_lang
@@ -596,9 +739,24 @@ class Post(object):
link = urljoin(self.base_url, link[1:])
index_len = len(self.index_file)
if self.strip_indexes and link[-(1 + index_len):] == '/' + self.index_file:
- return link[:-index_len]
- else:
- return link
+ link = link[:-index_len]
+ if query:
+ link = link + "?" + query
+ return link
+
+ @property
+ def previewimage(self, lang=None):
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+
+ image_path = self.meta[lang]['previewimage']
+
+ if not image_path:
+ return None
+
+ # This is further parsed by the template, because we don’t have access
+ # to the URL replacer here. (Issue #1473)
+ return image_path
def source_ext(self, prefix=False):
"""
@@ -679,6 +837,8 @@ def _get_metadata_from_file(meta_data):
>>> g = _get_metadata_from_file
>>> list(g([]).values())
[]
+ >>> str(g(["======","FooBar","======"])["title"])
+ 'FooBar'
>>> str(g(["FooBar","======"])["title"])
'FooBar'
>>> str(g(["#FooBar"])["title"])
@@ -714,6 +874,10 @@ def _get_metadata_from_file(meta_data):
if re_rst_title.findall(line) and i > 0:
meta['title'] = meta_data[i - 1].strip()
if 'title' not in meta:
+ if (re_rst_title.findall(line) and i >= 0 and
+ re_rst_title.findall(meta_data[i + 2])):
+ meta['title'] = meta_data[i + 1].strip()
+ if 'title' not in meta:
if re_md_title.findall(line):
meta['title'] = re_md_title.findall(line)[0]
@@ -726,6 +890,7 @@ def _get_metadata_from_file(meta_data):
def get_metadata_from_meta_file(path, config=None, lang=None):
"""Takes 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)
@@ -746,8 +911,12 @@ def get_metadata_from_meta_file(path, config=None, lang=None):
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)
+ 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) = [
@@ -770,7 +939,7 @@ def get_metadata_from_meta_file(path, config=None, lang=None):
if _type:
meta['type'] = _type
- return meta
+ return meta, newstylemeta
elif lang:
# Metadata file doesn't exist, but not default language,
@@ -778,7 +947,7 @@ def get_metadata_from_meta_file(path, config=None, lang=None):
# This makes the 2-file format detection more reliable (Issue #525)
return get_metadata_from_meta_file(path, config, lang=None)
else:
- return {}
+ return {}, True
def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
@@ -797,18 +966,24 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None)
except AttributeError:
config = None
- meta.update(get_metadata_from_meta_file(post.metadata_path, config, lang))
+ _, newstylemeta = get_metadata_from_meta_file(post.metadata_path, config, lang)
+ meta.update(_)
- if meta:
- return meta
- post.is_two_file = False
+ if not meta:
+ post.is_two_file = False
if file_metadata_regexp is not None:
meta.update(_get_metadata_from_filename_by_regex(post.source_path,
file_metadata_regexp,
unslugify_titles))
- meta.update(get_metadata_from_file(post.source_path, config, lang))
+ if getattr(post, 'compiler', None):
+ compiler_meta = post.compiler.read_metadata(post, file_metadata_regexp, unslugify_titles, lang)
+ meta.update(compiler_meta)
+
+ if not post.is_two_file:
+ # Meta file has precedence over file, which can contain garbage.
+ meta.update(get_metadata_from_file(post.source_path, config, lang))
if lang is None:
# Only perform these checks for the default language
@@ -823,15 +998,33 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None)
meta['title'] = os.path.splitext(
os.path.basename(post.source_path))[0]
- return meta
+ return meta, newstylemeta
-def hyphenate(dom, lang):
- if pyphen is not None:
- hyphenator = pyphen.Pyphen(lang=lang)
+def hyphenate(dom, _lang):
+ # circular import prevention
+ from .nikola import LEGAL_VALUES
+ lang = LEGAL_VALUES['PYPHEN_LOCALES'].get(_lang, pyphen.language_fallback(_lang))
+ if pyphen is not None and lang is not None:
+ # If pyphen does exist, we tell the user when configuring the site.
+ # If it does not support a language, we ignore it quietly.
+ try:
+ hyphenator = pyphen.Pyphen(lang=lang)
+ except KeyError:
+ LOGGER.error("Cannot find hyphenation dictoniaries for {0} (from {1}).".format(lang, _lang))
+ LOGGER.error("Pyphen cannot be installed to ~/.local (pip install --user).")
for tag in ('p', 'li', 'span'):
for node in dom.xpath("//%s[not(parent::pre)]" % tag):
- insert_hyphens(node, hyphenator)
+ skip_node = False
+ skippable_nodes = ['kbd', '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', [])):
+ skip_node = True
+ elif 'math' in node.get('class', []):
+ skip_node = True
+ if not skip_node:
+ insert_hyphens(node, hyphenator)
return dom
diff --git a/nikola/utils.py b/nikola/utils.py
index 87826ff..3708775 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,32 +27,66 @@
"""Utility functions."""
from __future__ import print_function, unicode_literals, absolute_import
-from collections import defaultdict, Callable
import calendar
import datetime
import dateutil.tz
import hashlib
+import io
import locale
import logging
+import natsort
import os
import re
import json
import shutil
import subprocess
import sys
-from zipfile import ZipFile as zipf
-try:
- from imp import reload
-except ImportError:
- pass
-
import dateutil.parser
import dateutil.tz
import logbook
+import warnings
+import PyRSS2Gen as rss
+from collections import defaultdict, Callable
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 pkg_resources import resource_filename
+from doit.cmdparse import CmdParse
from nikola import DEBUG
+__all__ = ['CustomEncoder', 'get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree',
+ 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters',
+ 'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path',
+ '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary',
+ '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', 'indent']
+
+# 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
@@ -72,9 +106,9 @@ def get_logger(name, handlers):
l = logbook.Logger(name)
for h in handlers:
if isinstance(h, list):
- l.handlers = h
+ l.handlers += h
else:
- l.handlers = [h]
+ l.handlers.append(h)
return l
@@ -97,9 +131,6 @@ else:
logging.basicConfig(level=logging.INFO)
-import warnings
-
-
def showwarning(message, category, filename, lineno, file=None, line=None):
"""Show a warning (from the warnings subsystem) to the user."""
try:
@@ -156,39 +187,8 @@ def req_missing(names, purpose, python=True, optional=False):
return msg
-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
-
-from doit import tools
-from unidecode import unidecode
-from pkg_resources import resource_filename
-from nikola import filters as task_filters
-
-import PyRSS2Gen as rss
-
-__all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree',
- 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters',
- 'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path',
- '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary',
- 'TranslatableSetting', 'TemplateHookRegistry', 'LocaleBorg',
- 'sys_encode', 'sys_decode', 'makedirs', 'get_parent_theme_name',
- 'demote_headers', 'get_translation_candidate', 'write_metadata',
- 'ask', 'ask_yesno']
-
-# Are you looking for 'generic_rss_renderer'?
-# It's defined in nikola.nikola.Nikola (the site object).
-
+from nikola import filters as task_filters # NOQA
ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding
@@ -208,11 +208,20 @@ def sys_decode(thing):
def makedirs(path):
"""Create a folder."""
- if not path or os.path.isdir(path):
+ if not path:
return
if os.path.exists(path):
- raise OSError('Path {0} already exists and is not a folder.')
- os.makedirs(path)
+ if not os.path.isdir(path):
+ raise OSError('Path {0} already exists and is not a folder.'.format(path))
+ else:
+ return
+ try:
+ os.makedirs(path)
+ return
+ except Exception:
+ if os.path.isdir(path):
+ return
+ raise
class Functionary(defaultdict):
@@ -368,8 +377,10 @@ class TranslatableSetting(object):
for a in f[0] + tuple(f[1].values()):
if isinstance(a, dict):
langkeys += list(a)
+
# Now that we know all this, we go through all the languages we have.
allvalues = set(keys + langkeys + list(self.values))
+ self.values['__orig__'] = self.values[self.default_lang]
for l in allvalues:
if l in keys:
oargs, okwargs = formats[l]
@@ -383,19 +394,22 @@ class TranslatableSetting(object):
# We create temporary TranslatableSettings and replace the
# values with them.
if isinstance(a, dict):
- a = TranslatableSetting('NULL', a)
+ a = TranslatableSetting('NULL', a, self.translations)
args.append(a(l))
else:
args.append(a)
for k, v in okwargs.items():
if isinstance(v, dict):
- v = TranslatableSetting('NULL', v)
+ v = TranslatableSetting('NULL', v, self.translations)
kwargs.update({k: v(l)})
else:
kwargs.update({k: v})
- self.values[l] = self.values[l].format(*args, **kwargs)
+ if l in self.values:
+ self.values[l] = self.values[l].format(*args, **kwargs)
+ else:
+ self.values[l] = self.values['__orig__'].format(*args, **kwargs)
self.values.default_factory = lambda: self.values[self.default_lang]
return self
@@ -472,7 +486,7 @@ class TemplateHookRegistry(object):
self._items.append((c, inp, wants_site_and_context, args, kwargs))
def __hash__(self):
- return config_changed({self.name: self._items})
+ return hash(config_changed({self.name: self._items})._calc_digest())
def __str__(self):
return '<TemplateHookRegistry: {0}>'.format(self._items)
@@ -490,6 +504,12 @@ class CustomEncoder(json.JSONEncoder):
class config_changed(tools.config_changed):
""" A copy of doit's but using pickle instead of serializing manually."""
+ def __init__(self, config, identifier=None):
+ super(config_changed, self).__init__(config)
+ self.identifier = '_config_changed'
+ if identifier is not None:
+ self.identifier += ':' + identifier
+
def _calc_digest(self):
if isinstance(self.config, str):
return self.config
@@ -507,6 +527,16 @@ class config_changed(tools.config_changed):
'{0}, must be string or dict'.format(type(
self.config)))
+ def configure_task(self, task):
+ task.value_savers.append(lambda: {self.identifier: self._calc_digest()})
+
+ def __call__(self, task, values):
+ """Return True if config values are unchanged."""
+ last_success = values.get(self.identifier)
+ if last_success is None:
+ return False
+ return (last_success == self._calc_digest())
+
def __repr__(self):
return "Change with config: {0}".format(json.dumps(self.config,
cls=CustomEncoder))
@@ -576,7 +606,7 @@ def load_messages(themes, translations, default_lang):
and "younger" themes have priority.
"""
messages = Functionary(dict, default_lang)
- oldpath = sys.path[:]
+ oldpath = list(sys.path)
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('base'), 'messages')
@@ -587,7 +617,7 @@ def load_messages(themes, translations, default_lang):
try:
translation = __import__('messages_' + lang)
# If we don't do the reload, the module is cached
- reload(translation)
+ _reload(translation)
if sorted(translation.MESSAGES.keys()) !=\
sorted(english.MESSAGES.keys()) and \
lang not in warned:
@@ -877,7 +907,10 @@ def get_crumbs(path, is_file=False, index_folder=None):
for i, crumb in enumerate(crumbs[::-1]):
if folder[-1] == os.sep:
folder = folder[:-1]
- index_post = index_folder.parse_index(folder)
+ # We don't care about the created Post() object except for its title;
+ # hence, the input_folder and output_folder given to
+ # index_folder.parse_index() don't matter
+ index_post = index_folder.parse_index(folder, '', '')
folder = folder.replace(crumb, '')
if index_post:
crumb = index_post.title() or crumb
@@ -1038,6 +1071,25 @@ class LocaleBorg(object):
return s
+class ExtendedRSS2(rss.RSS2):
+ xsl_stylesheet_href = None
+
+ def publish(self, handler):
+ if self.xsl_stylesheet_href:
+ handler.processingInstruction("xml-stylesheet", 'type="text/xsl" href="{0}" media="all"'.format(self.xsl_stylesheet_href))
+ # old-style class in py2
+ rss.RSS2.publish(self, handler)
+
+ def publish_extensions(self, handler):
+ if self.self_url:
+ handler.startElement("atom:link", {
+ 'href': self.self_url,
+ 'rel': "self",
+ 'type': "application/rss+xml"
+ })
+ handler.endElement("atom:link")
+
+
class ExtendedItem(rss.RSSItem):
def __init__(self, **kw):
@@ -1096,8 +1148,13 @@ def get_root_dir():
"""Find root directory of nikola installation by looking for conf.py"""
root = os.getcwd()
+ if sys.version_info[0] == 2:
+ confname = b'conf.py'
+ else:
+ confname = 'conf.py'
+
while True:
- if os.path.exists(os.path.join(root, 'conf.py')):
+ if os.path.exists(os.path.join(root, confname)):
return root
else:
basedir = os.path.split(root)[0]
@@ -1174,7 +1231,7 @@ def get_translation_candidate(config, path, lang):
def write_metadata(data):
"""Write metadata."""
- order = ('title', 'slug', 'date', 'tags', 'link', 'description', 'type')
+ order = ('title', 'slug', 'date', 'tags', 'category', 'link', 'description', 'type')
f = '.. {0}: {1}'
meta = []
for k in order:
@@ -1184,8 +1241,8 @@ def write_metadata(data):
pass
# Leftover metadata (user-specified/non-default).
- for k, v in data.items():
- meta.append(f.format(k, v))
+ for k in natsort.natsorted(list(data.keys()), alg=natsort.ns.F | natsort.ns.IC):
+ meta.append(f.format(k, data[k]))
meta.append('')
@@ -1198,7 +1255,10 @@ def ask(query, default=None):
default_q = ' [{0}]'.format(default)
else:
default_q = ''
- inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip()
+ 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()
if inp or default is None:
return inp
else:
@@ -1213,7 +1273,10 @@ def ask_yesno(query, default=None):
default_q = ' [Y/n]'
elif default is False:
default_q = ' [y/N]'
- inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q)).strip()
+ if 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()
if inp:
return inp.lower().startswith('y')
elif default is not None:
@@ -1223,10 +1286,6 @@ def ask_yesno(query, default=None):
return ask_yesno(query, default)
-from nikola.plugin_categories import Command
-from doit.cmdparse import CmdParse
-
-
class CommandWrapper(object):
"""Converts commands into functions."""
@@ -1253,32 +1312,58 @@ class Commands(object):
>>> commands.check(list=True) # doctest: +SKIP
"""
- def __init__(self, main):
+ def __init__(self, main, config, doitargs):
"""Takes a main instance, works as wrapper for commands."""
self._cmdnames = []
- for k, v in main.get_commands().items():
- self._cmdnames.append(k)
+ self._main = main
+ self._config = config
+ self._doitargs = doitargs
+ try:
+ cmdict = self._doitargs['cmds'].to_dict()
+ except AttributeError: # not a doit PluginDict
+ cmdict = self._doitargs['cmds']
+ for k, v in cmdict.items():
+ # 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)
+
+ try:
+ # nikola command: already instantiated (singleton)
+ opt = v.get_options()
+ except TypeError:
+ # doit command: needs some help
+ opt = v(config=self._config, **self._doitargs).get_options()
nc = type(
k2,
(CommandWrapper,),
{
- '__doc__': options2docstring(k, main.sub_cmds[k].options)
+ '__doc__': options2docstring(k, opt)
})
setattr(self, k, nc(k, self))
- self.main = main
def _run(self, cmd_args):
- self.main.run(cmd_args)
+ self._main.run(cmd_args)
def _run_with_kw(self, cmd, *a, **kw):
- cmd = self.main.sub_cmds[cmd]
- options, _ = CmdParse(cmd.options).parse([])
+ # cyclic import hack
+ from nikola.plugin_categories import Command
+ try:
+ cmd = self._doitargs['cmds'].get_plugin(cmd)
+ except AttributeError: # not a doit PluginDict
+ cmd = self._doitargs['cmds'][cmd]
+ try:
+ opt = cmd.get_options()
+ except TypeError:
+ cmd = cmd(config=self._config, **self._doitargs)
+ opt = cmd.get_options()
+
+ options, _ = CmdParse(opt).parse([])
options.update(kw)
if isinstance(cmd, Command):
cmd.execute(options=options, args=a)
@@ -1305,3 +1390,249 @@ def options2docstring(name, options):
for opt in options:
result.append('{0} type {1} default {2}'.format(opt.name, opt.type.__name__, opt.default))
return '\n'.join(result)
+
+
+class NikolaPygmentsHTML(HtmlFormatter):
+ """A Nikola-specific modification of Pygments’ HtmlFormatter."""
+ def __init__(self, anchor_ref, classes=None, linenos='table', linenostart=1):
+ if classes is None:
+ classes = ['code', 'literal-block']
+ self.nclasses = classes
+ super(NikolaPygmentsHTML, self).__init__(
+ cssclass='code', linenos=linenos, linenostart=linenostart, nowrap=False,
+ lineanchors=slugify(anchor_ref, force=True), anchorlinenos=True)
+
+ def wrap(self, source, outfile):
+ """
+ Wrap the ``source``, which is a generator yielding
+ individual lines, in custom generators.
+ """
+
+ style = []
+ if self.prestyles:
+ style.append(self.prestyles)
+ if self.noclasses:
+ style.append('line-height: 125%')
+ style = '; '.join(style)
+ classes = ' '.join(self.nclasses)
+
+ yield 0, ('<pre class="{0}"'.format(classes) + (style and ' style="{0}"'.format(style)) + '>')
+ for tup in source:
+ yield tup
+ yield 0, '</pre>'
+
+
+def get_displayed_page_number(i, num_pages, site):
+ if not i:
+ i = 0
+ if site.config["INDEXES_STATIC"]:
+ return i if i > 0 else num_pages
+ else:
+ return i + 1 if site.config["INDEXES_PAGES_MAIN"] else i
+
+
+def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force_addition=False, extension=None):
+ index_file = site.config["INDEX_FILE"]
+ if i or force_addition:
+ path_list = list(path_list)
+ if force_addition and not i:
+ i = 0
+ if not extension:
+ _, extension = os.path.splitext(index_file)
+ if len(path_list) > 0 and path_list[-1] == '':
+ path_list[-1] = index_file
+ elif len(path_list) == 0 or not path_list[-1].endswith(extension):
+ 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)):
+ path_schema = [path_schema]
+ else:
+ path_schema = None
+ if path_schema is not None:
+ del path_list[-1]
+ for entry in path_schema:
+ path_list.append(entry.format(number=displayed_i, old_number=i, index_file=index_file))
+ else:
+ path_list[-1] = '{0}-{1}{2}'.format(os.path.splitext(path_list[-1])[0], i, extension)
+ return path_list
+
+
+def os_path_split(path):
+ result = []
+ while True:
+ previous_path = path
+ path, tail = os.path.split(path)
+ if path == previous_path and tail == '':
+ result.insert(0, path)
+ break
+ result.insert(0, tail)
+ if len(path) == 0:
+ break
+ return result
+
+
+def adjust_name_for_index_path(name, i, displayed_i, lang, site, force_addition=False, extension=None):
+ return os.path.join(*adjust_name_for_index_path_list(os_path_split(name), i, displayed_i, lang, site, force_addition, extension))
+
+
+def adjust_name_for_index_link(name, i, displayed_i, lang, site, force_addition=False, extension=None):
+ link = adjust_name_for_index_path_list(name.split('/'), i, displayed_i, lang, site, force_addition, extension)
+ if not extension == ".atom":
+ if len(link) > 0 and link[-1] == site.config["INDEX_FILE"] and site.config["STRIP_INDEXES"]:
+ link[-1] = ''
+ return '/'.join(link)
+
+
+def create_redirect(src, dst):
+ 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'
+ '<title>Redirecting...</title>\n<meta name="robots" '
+ 'content="noindex">\n<meta http-equiv="refresh" content="0; '
+ 'url={0}">\n</head>\n<body>\n<p>Page moved '
+ '<a href="{0}">here</a>.</p>\n</body>'.format(dst))
+
+
+class TreeNode(object):
+ 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):
+ self.name = name
+ self.parent = parent
+ self.children = []
+
+ def get_path(self):
+ path = []
+ curr = self
+ while curr is not None:
+ path.append(curr)
+ curr = curr.parent
+ return reversed(path)
+
+ def get_children(self):
+ return self.children
+
+
+def flatten_tree_structure(root_list):
+ 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):
+ 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):
+ def escape(s):
+ return s.replace('\\', '\\\\').replace('/', '\\/')
+
+ return '/'.join([escape(p) for p in category_path])
+
+
+# Stolen from textwrap in Python 3.4.3.
+def indent(text, prefix, predicate=None):
+ """Adds 'prefix' to the beginning of selected lines in 'text'.
+
+ If 'predicate' is provided, 'prefix' will only be added to the lines
+ where 'predicate(line)' is True. If 'predicate' is not provided,
+ it will default to adding 'prefix' to all non-empty lines that do not
+ consist solely of whitespace characters.
+ """
+ if predicate is None:
+ def predicate(line):
+ return line.strip()
+
+ def prefixed_lines():
+ for line in text.splitlines(True):
+ yield (prefix + line if predicate(line) else line)
+ return ''.join(prefixed_lines())
diff --git a/nikola/winutils.py b/nikola/winutils.py
index 712de39..8e29f5b 100644
--- a/nikola/winutils.py
+++ b/nikola/winutils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2014 Roberto Alsina and others.
+# Copyright © 2012-2015 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,6 +29,7 @@
from __future__ import print_function, unicode_literals
import os
import shutil
+import io
# don't add imports to nikola code, will be imported in setup.py
@@ -63,9 +64,14 @@ def fix_all_git_symlinked(topdir):
Weakness: if interrupted of fail amidst a directory copy, next run will not
see the missing files.
"""
- with open(topdir + r'\nikola\data\symlinked.txt', 'rb') as f:
- all_bytes = f.read()
- text = all_bytes.decode('utf8')
+ # 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:
+ 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:
+ text = f.read()
# expect each line a relpath from git or zip root,
# smoke test relpaths are relative to git root
if text.startswith('.'):
@@ -86,7 +92,7 @@ def fix_all_git_symlinked(topdir):
continue
# build src path and do some basic validation
- with open(os.path.join(topdir, dst), 'r') as f:
+ with io.open(os.path.join(topdir, dst), 'r', encoding='utf-8') as f:
text = f.read()
dst_dir = os.path.dirname(dst)
try: